Compare commits

...

116 Commits
v0.0.3 ... main

Author SHA1 Message Date
Vadim Bauer ff1485171c
chnage robot account to supported
Signed-off-by: Vadim Bauer <vb@container-registry.com>
2025-08-05 15:07:46 +02:00
RafsanNeloy e15bde4ba0
Typo solved (#522) 2025-08-01 18:18:25 +05:30
Patrick Eschenbach 3c8e6239a0
Add Support for System-Level Robot Accounts (#507) 2025-07-29 18:33:44 +05:30
dependabot[bot] 6caed4d4f4
build(deps): bump golang.org/x/text from 0.23.0 to 0.27.0 (#519)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.23.0 to 0.27.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.23.0...v0.27.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-version: 0.27.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-21 14:32:23 +05:30
dependabot[bot] 18cb2da313
build(deps): bump github.com/charmbracelet/bubbletea from 1.3.5 to 1.3.6 (#515) 2025-07-15 13:51:06 +00:00
dependabot[bot] 85618bdd6b
build(deps): bump golang.org/x/term from 0.32.0 to 0.33.0 (#514)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.32.0 to 0.33.0.
- [Commits](https://github.com/golang/term/compare/v0.32.0...v0.33.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-version: 0.33.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-15 15:46:31 +02:00
Patrick Eschenbach 0fbd8ecdea
Increased the logs follow refresh interval to 5s as a default (#516)
* Increased the logs follow refresh interval to 5s as a default

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Re-export docs

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

---------

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>
2025-07-15 15:45:27 +02:00
Patrick Eschenbach c705221645
Replication Start/Stop Management (#501)
* First save

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added replication list command

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Split replication policies to sub command

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added replication policies view command; added get rep policies to prompt and handler

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Finished rpolicies view command; started create command

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added create command; ToDo: finish all fields for creation

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Made changes to satisfy linter

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* feat(replication): add commands to start and stop replication executions

- Implement start command to initiate policy execution
- Add stop command to halt running executions
- Create replication execution views
- Update API handler with execution management functions
- Extend prompt functionality for execution commands

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added long docs

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added changes for linter; removed unneeded funcs

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

---------

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>
2025-07-15 15:19:14 +02:00
Patrick Eschenbach cc2113c2dd
Replication Policy Management with Configuration File Support (#510)
* feat(replication): add complete replication policy management with config files

- YAML/JSON configuration file support
- Comprehensive filter validation (resource/name/tag/label)
- Enhanced policy viewing with separate filter tables
- Support for manual/scheduled/event-based triggers
- Example configs and documentation included

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Updated docs for linter

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

---------

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>
2025-07-15 15:16:06 +02:00
Patrick Eschenbach 3304c2ea0f
feat(logs): add audit logs command with follow support (#511)
* add: audit-log command

Signed-off-by: bupd <bupdprasanth@gmail.com>
Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Update cmd/harbor/root/logs.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Vadim Bauer <Bauer.vadim@gmail.com>
Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* build(deps): bump github.com/goharbor/go-client from 0.210.0 to 0.213.1

Bumps [github.com/goharbor/go-client](https://github.com/goharbor/go-client) from 0.210.0 to 0.213.1.
- [Release notes](https://github.com/goharbor/go-client/releases)
- [Commits](https://github.com/goharbor/go-client/compare/v0.210.0...v0.213.1)

---
updated-dependencies:
- dependency-name: github.com/goharbor/go-client
  dependency-version: 0.213.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* fix(cli): adapt to go-client v0.213.1 changes

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added follow logs

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Moved to AuditLogExt; follow does not yet work

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Logrus output is in verbose

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added trailing functionality to logs command; moved endpoint to ext audit log

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* fix: output formatting

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Changes to satisfy linter

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

---------

Signed-off-by: bupd <bupdprasanth@gmail.com>
Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>
Signed-off-by: Vadim Bauer <Bauer.vadim@gmail.com>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: bupd <bupdprasanth@gmail.com>
Co-authored-by: Vadim Bauer <Bauer.vadim@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-15 15:10:12 +02:00
dependabot[bot] fd38240593
build(deps): bump github.com/goharbor/go-client from 0.210.0 to 0.213.1 (#509)
* build(deps): bump github.com/goharbor/go-client from 0.210.0 to 0.213.1

Bumps [github.com/goharbor/go-client](https://github.com/goharbor/go-client) from 0.210.0 to 0.213.1.
- [Release notes](https://github.com/goharbor/go-client/releases)
- [Commits](https://github.com/goharbor/go-client/compare/v0.210.0...v0.213.1)

---
updated-dependencies:
- dependency-name: github.com/goharbor/go-client
  dependency-version: 0.213.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* fix(cli): adapt to go-client v0.213.1 changes

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>
2025-07-15 15:05:31 +02:00
Chethan 97fc4d6c64
Change the print statements in func preblock() (#513)
Signed-off-by: chethanm99 <chethanm1399@gmail.com>
2025-07-15 18:31:08 +05:30
Patrick Eschenbach 598b817a32
Replication Policy Management (#499)
* First save

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added replication list command

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Split replication policies to sub command

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added replication policies view command; added get rep policies to prompt and handler

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added replication policy delete command

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Finished rpolicies view command; started create command

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added create command; ToDo: finish all fields for creation

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Made changes to satisfy linter

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added replication policies docs

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added replication policies update command

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Fix: left alignment in create and update view

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Fix: left alignment in create and update view

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Remove replication mode from update view

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added long docs

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added long docs

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Made changes to satisfy linter

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Made changes to satisfy linter

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Made changes to satisfy linter

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* fix(replication): handle deprecated deletion field in policy updates

Set both ReplicateDeletion (new) and Deletion (deprecated) fields
to ensure compatibility with all Harbor API versions during policy
updates. The Deletion field will be removed in future Harbor versions.

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added missing replication filters to creation and update view

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Export docs after rebase main

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Remove unnecessary stdout

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Put filters to their own table

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

---------

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>
2025-07-02 17:00:19 +02:00
T3rm1n4t0r 95edbd6f8b
enhancing artifact view command (#485)
Co-authored-by: Prasanth Baskar <bupdprasanth@gmail.com>
2025-07-02 17:23:04 +05:30
Patrick Eschenbach af41e84a14
Enhanced Robot Account Configuration Format (#504)
* Added namespace and slighlty changed robot config spec

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Changed format of robot permission model to prepare for system robots

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Made changes to satisfy linter

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Changed kind to level

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

---------

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>
2025-07-02 08:49:30 +02:00
Prasanth Baskar 81ab6301fe
feat: add go report card (#506) 2025-06-28 08:14:26 +02:00
Chetan 7805ccdb2a
fix: pagination in repo list command (#502)
Co-authored-by: Prasanth Baskar <bupdprasanth@gmail.com>
2025-06-27 06:08:36 +05:30
Patrick Eschenbach 8fe88a3fb3
Improve table grid controls and form validation (Follow‑up Improvements to PR #489) (#503)
* feat(ui): improve table grid controls and form validation

- refactor(tablegrid): change toggle controls to explicit on/off actions
- feat(tablegrid): add table-wide toggle shortcuts (ctrl+a, ctrl+d)
- fix(robot): improve validation for robot creation expiration time
- fix(cmd): convert Run to RunE for proper error handling in robot list
- docs(ui): update keyboard shortcut documentation in footer text

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added changes to satisfy linter

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added readme.md for robot config file

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

---------

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>
Co-authored-by: Prasanth Baskar <bupdprasanth@gmail.com>
2025-06-26 13:41:58 +02:00
Patrick Eschenbach c26c926fe3
Rebase add scan all command (#490)
Co-authored-by: muaz-32 <96006730+muaz-32@users.noreply.github.com>
Co-authored-by: Prasanth Baskar <bupdprasanth@gmail.com>
2025-06-26 15:38:19 +05:30
Devesh chouhan 8aa4a2ca84
fix: default FormatUrl to use https instead of http (#496)
Co-authored-by: Prasanth Baskar <bupdprasanth@gmail.com>
2025-06-26 02:24:55 +05:30
Patrick Eschenbach 2fd435940d
Finalizing Robots Account cli command (#489)
Co-authored-by: bupd <bupdprasanth@gmail.com>
2025-06-24 18:58:48 +05:30
Prasanth Baskar c92eb8f687
feat: Release to Homebrew-Tap (#421) 2025-06-20 15:22:25 +05:30
hippie-danish 9929fd959a
fix velnerability-check (#494) 2025-06-20 01:43:57 +05:30
Meeth Panjwani 1fbe629fef
improved error messages for login command (#488)
Signed-off-by: meethereum <meethbackup@gmail.com>
Co-authored-by: Prasanth Baskar <bupdprasanth@gmail.com>
2025-06-17 15:23:35 +02:00
Alok 700c075be6
Fix: Ensure `project create cmd` asks for necessary options (#447)
Co-authored-by: bupd <bupdprasanth@gmail.com>
2025-06-10 18:44:10 +05:30
Patrick Eschenbach ba767105b0
Added docs for config and encryption here from website (#487)
* Added docs for config and encryption here from website

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added more clear info to encryption docs about ci/cd and production environments

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* This index for the harborcli docs also has to live here

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

---------

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>
2025-06-05 21:33:18 +05:30
Prasanth Baskar ce4b52f41e
fix: remove codecov on main (#486) 2025-06-03 19:58:51 +05:30
Rizul Gupta 60ad0bda48
feat: Add `label` sub-cmd in `artifact` cmd (#483)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Prasanth Baskar <bupdprasanth@gmail.com>
2025-06-03 19:15:36 +05:30
Rizul Gupta 6caf59e1af
feat: add `project config` command (#448)
* Added the metadata command inside project command.
Signed-off-by: muaz-32 <96006730+muaz-32@users.noreply.github.com>

* Added the option for whether the argument is project ID or not.

It is beneficial when a project's name is same as another project's ID.
Signed-off-by: muaz-32 <96006730+muaz-32@users.noreply.github.com>

* Add `delete` command for metadata.

Signed-off-by: muaz-32 <96006730+muaz-32@users.noreply.github.com>

* Add `view` command for metadata.

Signed-off-by: muaz-32 <96006730+muaz-32@users.noreply.github.com>

* Updated the structure according to newer changes and added the `update` and `list` command.

Signed-off-by: muaz-32 <96006730+muaz-32@users.noreply.github.com>

* Refactored the code. Remove duplicate code by bringing the declaration and addition of the flag in the command in the `cmd.go` file.

Signed-off-by: muaz-32 <96006730+muaz-32@users.noreply.github.com>

* Updated the client connecting code.

Signed-off-by: muaz-32 <96006730+muaz-32@users.noreply.github.com>

* Refactored the code.

Signed-off-by: muaz-32 <96006730+muaz-32@users.noreply.github.com>

* Improved the metadata presentation format.

Signed-off-by: muaz-32 <96006730+muaz-32@users.noreply.github.com>

* first commit for intial rename

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* lint fixes

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* lint fixes

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* remove delete config cmd

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* improve list config cmd

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* improve list config cmd

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* add persistent flag

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* modify add cmd

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* merge main

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* lint fix

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* add update command

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* update error handling

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* add flags in update

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* Update cmd/harbor/root/project/config/update.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Rizul Gupta <112455393+rizul2108@users.noreply.github.com>

* fix errors

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* resolve errors

resolve errors

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

add docs

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

add docs

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

update docs

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* update desc

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

---------

Signed-off-by: muaz-32 <96006730+muaz-32@users.noreply.github.com>
Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>
Signed-off-by: Rizul Gupta <112455393+rizul2108@users.noreply.github.com>
Co-authored-by: muaz-32 <96006730+muaz-32@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-27 15:35:50 +02:00
Ujjwal Sharma ae0c3df5ce
adds changes suggested in context command (#482)
* adds changes

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>

* small change

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>

---------

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>
2025-05-27 15:11:12 +02:00
Ujjwal Sharma 87b57b0760
removes context switch via login command (#477)
* removes context switch via login command

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>

* updates docs

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>

* updates tests

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>

* fix tests

Signed-off-by: Ujjwal Sharma <68021601+Darkhood148@users.noreply.github.com>

---------

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>
Signed-off-by: Ujjwal Sharma <68021601+Darkhood148@users.noreply.github.com>
2025-05-27 15:08:55 +02:00
Patrick Eschenbach 7d68faf5a1
Fix: Update error handling in projectName retrieval for tags list
Signed-off-by: Patrick Eschenbach <45457307+qcserestipy@users.noreply.github.com>
2025-05-26 21:01:16 +02:00
Patrick Eschenbach a1df056b6f
Added return for empty reponame (#452)
Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>
2025-05-26 20:57:34 +02:00
T3rm1n4t0r a813bcfa6a
feat: fixing the pointer positioning with change in context (#481)
Signed-off-by: PrathamX595 <pratham.seth.cer23@itbhu.ac.in>
2025-05-26 19:54:07 +02:00
Patrick Eschenbach 968a772377
Update README.md to fit correct badge
Signed-off-by: Patrick Eschenbach <45457307+qcserestipy@users.noreply.github.com>
2025-05-23 14:43:51 +02:00
Patrick Eschenbach 2d678109a0
Unit testing Coverage report (#376)
* Resolve merge conflicts

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added codecov badge for testing

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added codecov badge for testing

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added helpers package to context test after upstream rebase

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added changes to satisfy linter

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added dagger coverage steps to pipeline

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* ci(coverage): implement test coverage threshold check

add coverage threshold verification to dagger function
integrate coverage check into GitHub Actions pipeline
ensure proper syntax in shell script for accurate comparison
set initial coverage threshold at 80% for CI enforcement

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Fix failing test

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Cleanup client testing; added setconfig function to utils such that also in memeory config can be updated

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added changes to satisfy linter

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added test coverage entries to dagger readme

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added test coverage entries to dagger readme

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Test code cov token for upload

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Fix: wrong helper import in cmd test

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Update: test coverage report export

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Changed pipeline for test summary

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Changed pipeline for test summary

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Changed pipeline for test summary

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Adjusted dagger function for test report; added step summary; moved config cmd test from to context_test pkg

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added note about target coverage

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Readded coverage step for codecov upload

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Update cmd/harbor/root/repository/delete.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Patrick Eschenbach <45457307+qcserestipy@users.noreply.github.com>

* Update README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Patrick Eschenbach <45457307+qcserestipy@users.noreply.github.com>

* Update .dagger/README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Patrick Eschenbach <45457307+qcserestipy@users.noreply.github.com>

* Update pkg/utils/client.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Patrick Eschenbach <45457307+qcserestipy@users.noreply.github.com>

---------

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>
Signed-off-by: Patrick Eschenbach <45457307+qcserestipy@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-23 14:33:56 +02:00
Ujjwal Sharma 89d1f402e7
Adds harbor context command (#445)
* adds harbor context command

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>

* Update cmd/harbor/root/context/list.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Ujjwal Sharma <68021601+Darkhood148@users.noreply.github.com>

* renames config to context

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>

* linting changes

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>

* documentation changes

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>

* test changes

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>

* readd test; handle non-tty in bubble tea

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>

* rebase and lint

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>

* rebase and lint

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>

* rebase and lint

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>

* highlight active user

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>

* adds context switch command

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>

* updates logging

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>

* updates context switch

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>

* Update cmd/harbor/root/context/delete.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Ujjwal Sharma <68021601+Darkhood148@users.noreply.github.com>

* Update cmd/harbor/root/context/cmd.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Ujjwal Sharma <68021601+Darkhood148@users.noreply.github.com>

* minor change

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>

* rebasing

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>

* doc changes

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>

* linting changes

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>

* updates docs

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>

---------

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>
Signed-off-by: Ujjwal Sharma <68021601+Darkhood148@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-20 15:14:43 +02:00
Prasanth Baskar f492136dee
Fix: Doc generation script for reproducibility (#465)
* fix: doc generation script

Signed-off-by: bupd <bupdprasanth@gmail.com>

* update: cli-docs

Signed-off-by: bupd <bupdprasanth@gmail.com>

* update: man-docs

Signed-off-by: bupd <bupdprasanth@gmail.com>

* fix: workflow to check modified docs too

Signed-off-by: bupd <bupdprasanth@gmail.com>

* Update .github/workflows/default.yaml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Prasanth Baskar <bupdprasanth@gmail.com>

---------

Signed-off-by: bupd <bupdprasanth@gmail.com>
Signed-off-by: Prasanth Baskar <bupdprasanth@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-19 19:06:03 +05:30
meethereum 415e62a344
Fix user elevate (#473)
* fix: handle nil error to avoid panic

Signed-off-by: meethereum <meethbackup@gmail.com>

* log feedback if user does not exist

Signed-off-by: meethereum <meethbackup@gmail.com>

* fix error checking conditional

Signed-off-by: meethereum <meethbackup@gmail.com>

---------

Signed-off-by: meethereum <meethbackup@gmail.com>
2025-05-17 17:42:01 +02:00
Afeefuddin 2a06d9e1d8
fix: Show Banner messsage in system info instead of json (#471)
* fix: Show Banner messsage in system info instead of json

Signed-off-by: afeefuddin <afeefud2004din@gmail.com>

* Declare types on top of the file

Signed-off-by: afeefuddin <afeefud2004din@gmail.com>

---------

Signed-off-by: afeefuddin <afeefud2004din@gmail.com>
2025-05-17 11:03:09 +02:00
Afeefuddin 4accd4da76
Add groups to root help command (#470)
Signed-off-by: afeefuddin <afeefud2004din@gmail.com>
2025-05-17 11:02:27 +02:00
Chetan 62a20f0ddb
fix: PreviouslyLoggedIn should display context name (#466)
Signed-off-by: Chetan <jellybeans33124@gmail.com>
2025-05-15 17:37:57 +02:00
Vadim Bauer ff1425cc7f
docs: update existing and missing commands.
Signed-off-by: Vadim Bauer <vb@container-registry.com>
2025-05-13 19:13:51 +02:00
Vadim Bauer 5174b7a6ec
docs: update table
Signed-off-by: Vadim Bauer <vb@container-registry.com>
2025-05-13 19:08:53 +02:00
Vadim Bauer ffb9af0929
docs: update icon Update README.md
Signed-off-by: Vadim Bauer <vb@container-registry.com>
2025-05-13 19:05:34 +02:00
Vadim Bauer f40dbe3022 docs: update README to enhance clarity and detail on Harbor CLI features and usage 2025-05-13 19:01:59 +02:00
dependabot[bot] 254ed5758c
build(deps): bump github.com/charmbracelet/bubbletea from 1.3.4 to 1.3.5 (#449)
Bumps [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea) from 1.3.4 to 1.3.5.
- [Release notes](https://github.com/charmbracelet/bubbletea/releases)
- [Changelog](https://github.com/charmbracelet/bubbletea/blob/main/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/bubbletea/compare/v1.3.4...v1.3.5)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/bubbletea
  dependency-version: 1.3.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-13 16:00:06 +02:00
Prasanth Baskar 4f4ca29a26
Fix: Manpages Remove Dates (#451)
* fix: remove dates from man docs

Signed-off-by: bupd <bupdprasanth@gmail.com>

* fix: typo in man docs

Signed-off-by: bupd <bupdprasanth@gmail.com>

---------

Signed-off-by: bupd <bupdprasanth@gmail.com>
2025-05-13 15:17:38 +02:00
Afeefuddin fd0a626e61
Update the list of commands in the readme file (#454)
Signed-off-by: afeefuddin <afeefud2004din@gmail.com>
2025-05-13 15:07:11 +02:00
Rizul Gupta 4ace830f36
Add `scanner` cmd (#396)
* Added `scanner create` command.

Signed-off-by: muaz-32 <96006730+muaz-32@users.noreply.github.com>

* Improved the `scanner create` command. It tests the connection before the creation of the scanner.

Signed-off-by: muaz-32 <96006730+muaz-32@users.noreply.github.com>

* Update pkg/api/scanner_handler.go

Co-authored-by: Aman <136564604+amands98@users.noreply.github.com>
Signed-off-by: Muazul Islam <96006730+muaz-32@users.noreply.github.com>

* Added the code review suggestions.

Signed-off-by: muaz-32 <96006730+muaz-32@users.noreply.github.com>

* Added the `view` command for scanner.

Signed-off-by: muaz-32 <96006730+muaz-32@users.noreply.github.com>

* Added the `metadata` command for scanner.

Signed-off-by: muaz-32 <96006730+muaz-32@users.noreply.github.com>

* Added the `set-default` command for scanner.

Signed-off-by: muaz-32 <96006730+muaz-32@users.noreply.github.com>

* Added the `delete` command for scanner.

Signed-off-by: muaz-32 <96006730+muaz-32@users.noreply.github.com>

* Added the `update` command for scanner and fixed the `create` command for auth type.

Signed-off-by: muaz-32 <96006730+muaz-32@users.noreply.github.com>

* Added the `ping` option while creating scanner to mimic the web UI interaction.

Signed-off-by: muaz-32 <96006730+muaz-32@users.noreply.github.com>

* add header and gofmt

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* add docs

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* lint fixes

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* lint fixes

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* update list cmd

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* add header

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* improve ui

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* fix lint issues

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* Update pkg/views/scanner/list/view.go

Co-authored-by: Prasanth Baskar <bupdprasanth@gmail.com>
Signed-off-by: Rizul Gupta <112455393+rizul2108@users.noreply.github.com>

* Update cmd/harbor/root/cmd.go

Co-authored-by: Prasanth Baskar <bupdprasanth@gmail.com>
Signed-off-by: Rizul Gupta <112455393+rizul2108@users.noreply.github.com>

* fix: implement changes and apply lint fixes

implement changes

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

view-cmd terminal change fixed

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

lint fix

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* implement changes and update docs

2 changes implemented

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

fix create view

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

get scanners by name and fixes

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

improve desc and docs of cmds

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

small fix

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

small fixes in url

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

small fixes in url

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* small fix

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

---------

Signed-off-by: muaz-32 <96006730+muaz-32@users.noreply.github.com>
Signed-off-by: Muazul Islam <96006730+muaz-32@users.noreply.github.com>
Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>
Signed-off-by: Rizul Gupta <112455393+rizul2108@users.noreply.github.com>
Signed-off-by: Prasanth Baskar <bupdprasanth@gmail.com>
Co-authored-by: muaz-32 <96006730+muaz-32@users.noreply.github.com>
Co-authored-by: Aman <136564604+amands98@users.noreply.github.com>
Co-authored-by: Prasanth Baskar <bupdprasanth@gmail.com>
2025-05-13 15:06:51 +02:00
Prasanth Baskar be21559a27
Add: Quota Command to Manage Quotas (#97)
* add: quota `list` command

Signed-off-by: bupd <bupdprasanth@gmail.com>

* add: quota `view` command

Signed-off-by: bupd <bupdprasanth@gmail.com>

* add: quota `update` command

Signed-off-by: bupd <bupdprasanth@gmail.com>

* add: unit test for quota update

Signed-off-by: bupd <bupdprasanth@gmail.com>

* fix lint

Signed-off-by: bupd <bupdprasanth@gmail.com>

* fix quota list cmd

Signed-off-by: bupd <bupdprasanth@gmail.com>

* fix quota view cmd

Signed-off-by: bupd <bupdprasanth@gmail.com>

* update quota view

Signed-off-by: bupd <bupdprasanth@gmail.com>

* fix quota cmd

Signed-off-by: bupd <bupdprasanth@gmail.com>

* add docs

Signed-off-by: bupd <bupdprasanth@gmail.com>

* fix lint issues

Signed-off-by: bupd <bupdprasanth@gmail.com>

---------

Signed-off-by: bupd <bupdprasanth@gmail.com>
2025-05-13 15:06:29 +02:00
dependabot[bot] 99b1020d8a
build(deps): bump golang.org/x/term from 0.31.0 to 0.32.0 (#459)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.31.0 to 0.32.0.
- [Commits](https://github.com/golang/term/compare/v0.31.0...v0.32.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-version: 0.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-13 14:53:38 +02:00
Rizul Gupta 9265b36353
feat: Update info command (#453) 2025-05-13 07:50:45 +02:00
Rizul Gupta 2a84670e2f
feat: add webhook cmd (#391) 2025-05-13 07:49:58 +02:00
Rizul Gupta d8d1289ddc
Refactor: Make Error logging more user friendly for `project` commands (#424)
* fix error logging in project command

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* add logrus debug for verbose flag

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* lint fixes

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* code fixes

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* add verbose for list cmd

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* add verbose for llogs cmd

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* Update cmd/harbor/root/project/create.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Rizul Gupta <112455393+rizul2108@users.noreply.github.com>

* fix project create

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* Update cmd/harbor/root/project/create.go

Co-authored-by: Prasanth Baskar <bupdprasanth@gmail.com>
Signed-off-by: Rizul Gupta <112455393+rizul2108@users.noreply.github.com>

* implement change

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* disable logrus totally for non-verbose

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* handle prompt user aborting and deletion errors

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* fmt fix

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* Update cmd/harbor/root/tag/immutable/list.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Rizul Gupta <112455393+rizul2108@users.noreply.github.com>

* lint fix

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* lint fix

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

lint fix

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* Update cmd/harbor/root/artifact/view.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Rizul Gupta <112455393+rizul2108@users.noreply.github.com>

* small fixes

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* lint fixes

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

---------

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>
Signed-off-by: Rizul Gupta <112455393+rizul2108@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Prasanth Baskar <bupdprasanth@gmail.com>
2025-05-12 09:24:37 +02:00
Rizul Gupta 97c20c0209
feat: Add systemcve cli cmd (#388)
* created systemcve cmd

Signed-off-by: ALTHAF <althafasharaf02@gmail.com>

* modified systemcve cmd

Signed-off-by: ALTHAF <althafasharaf02@gmail.com>

* modified to harbor cve-allowlist list/add

Signed-off-by: ALTHAF <althafasharaf02@gmail.com>

* Update cmd.go

Signed-off-by: ALTHAF <114910365+Althaf66@users.noreply.github.com>

* fix lint issue

Signed-off-by: ALTHAF <althafasharaf02@gmail.com>

* changed date to iso format

Signed-off-by: ALTHAF <althafasharaf02@gmail.com>

* modified cveallowlist cmd

Signed-off-by: ALTHAF <althafasharaf02@gmail.com>

* add docs

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* fix lint issues

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* fix lint issues

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

fixes

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

fixes

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* header fixes

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* suggested changes

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* small changes

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* lint fix

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

---------

Signed-off-by: ALTHAF <althafasharaf02@gmail.com>
Signed-off-by: ALTHAF <114910365+Althaf66@users.noreply.github.com>
Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>
Signed-off-by: Vadim Bauer <vb@container-registry.com>
Co-authored-by: ALTHAF <althafasharaf02@gmail.com>
Co-authored-by: ALTHAF <114910365+Althaf66@users.noreply.github.com>
Co-authored-by: Vadim Bauer <vb@container-registry.com>
2025-04-29 15:29:51 +02:00
Prasanth Baskar 7fa59cdeff
Fix goreleaser (#442)
* update for release

Signed-off-by: bupd <bupdprasanth@gmail.com>

* add test release

Signed-off-by: bupd <bupdprasanth@gmail.com>

* fix version issues

Signed-off-by: bupd <bupdprasanth@gmail.com>

---------

Signed-off-by: bupd <bupdprasanth@gmail.com>
2025-04-29 15:25:48 +02:00
Rizul Gupta 1435a29efb
Improve Server Input Sanitization and Credential Name Generation (#443)
* modify validate func and improve suggestion

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* improve suggestions for cred name

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* Update pkg/utils/helper.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Rizul Gupta <112455393+rizul2108@users.noreply.github.com>

* improve check

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* implement changes

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* lint fix

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

---------

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>
Signed-off-by: Rizul Gupta <112455393+rizul2108@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-29 15:25:15 +02:00
Rizul Gupta 493665d4d8
Correct Artifact View arg parsing (#444)
* better authorizaion

Signed-off-by: Rohan <315scisyb2020rohanmishra@gmail.com>

* Fixed the bug

Signed-off-by: Rohan <315scisyb2020rohanmishra@gmail.com>

* improve view and list funcs

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* improve desc and docs

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* Update cmd/harbor/root/artifact/view.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Rizul Gupta <112455393+rizul2108@users.noreply.github.com>

---------

Signed-off-by: Rohan <315scisyb2020rohanmishra@gmail.com>
Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>
Signed-off-by: Rizul Gupta <112455393+rizul2108@users.noreply.github.com>
Co-authored-by: Rohan <315scisyb2020rohanmishra@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-29 15:24:13 +02:00
dependabot[bot] a12c359b55
build(deps): bump github.com/charmbracelet/huh from 0.6.0 to 0.7.0 (#433) 2025-04-22 14:18:14 +00:00
Prasanth Baskar 2258d255b4
fix lint (#439)
Signed-off-by: bupd <bupdprasanth@gmail.com>
2025-04-22 16:07:47 +02:00
Prasanth Baskar e7432058e8
Add Documentation for usage of dagger (#434)
* add readme for dagger

Signed-off-by: bupd <bupdprasanth@gmail.com>

* improve the readme of dagger

Signed-off-by: Prasanth Baskar <bupdprasanth@gmail.com>

---------

Signed-off-by: bupd <bupdprasanth@gmail.com>
Signed-off-by: Prasanth Baskar <bupdprasanth@gmail.com>
2025-04-22 15:57:12 +02:00
dependabot[bot] 93e38022ef
build(deps): bump github.com/charmbracelet/bubbles from 0.20.0 to 0.21.0 (#432)
Bumps [github.com/charmbracelet/bubbles](https://github.com/charmbracelet/bubbles) from 0.20.0 to 0.21.0.
- [Release notes](https://github.com/charmbracelet/bubbles/releases)
- [Changelog](https://github.com/charmbracelet/bubbles/blob/master/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/bubbles/compare/v0.20.0...v0.21.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/bubbles
  dependency-version: 0.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-22 15:55:02 +02:00
Rizul Gupta 8ae04ff88f
Fix: Preserve Table Alignment While Adding Status Colors (#430)
* fix health table

fix table

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

fix lint issues

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

fix lins

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* add json and yaml output

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* small changes

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

---------

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>
2025-04-22 15:54:43 +02:00
meethereum 50a8e01af6
fix: return proper error when a directory exists instead of config file (#422)
* fix: return proper error when a directory exists instead of config file

Signed-off-by: meethereum <meethbackup@gmail.com>

* Update error message to make it more clear

Co-authored-by: Prasanth Baskar <bupdprasanth@gmail.com>
Signed-off-by: meethereum <147987030+meethereum@users.noreply.github.com>

* handled error while checking config file

Signed-off-by: meethereum <meethbackup@gmail.com>

---------

Signed-off-by: meethereum <meethbackup@gmail.com>
Signed-off-by: meethereum <147987030+meethereum@users.noreply.github.com>
Co-authored-by: Prasanth Baskar <bupdprasanth@gmail.com>
2025-04-22 15:51:10 +02:00
Rizul Gupta 52a7b6cbc6
docs: add CONTRIBUTING.md for community contributions (#405)
* add CONTRIBUTING.md

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* suggested changes

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

---------

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>
2025-04-22 15:48:01 +02:00
Ujjwal Sharma 629e837f44
Adds Vulnerability Check workflow (#379)
* Adds Vulnerability Check workflow

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>

* Adds vulnerability check job in default github workflow

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>

* Adds description for vulnerability-check and vulnerability-check-report

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>

---------

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>
2025-04-22 15:45:15 +02:00
Rizul Gupta 010cabc252
Continuing Work from #110: add `instance` cmd (#389)
* created new instance cmd-create,list,delete

Signed-off-by: Althaf66 <althafasharaf02@gmail.com>

* modified list cmd

Signed-off-by: Althaf66 <althafasharaf02@gmail.com>

* small lint fixes

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* add docs

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* lint fixes

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

fix logic

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

fix lint issue

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* update desc for commands

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* small lint fix

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* update docs

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* Update cmd/harbor/root/instance/create.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Rizul Gupta <112455393+rizul2108@users.noreply.github.com>

* lint fix

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* add validators

validate url

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

add validate func in form

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

add validates in form for username and pwd

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

update docs

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* implement changes

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* small changes

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* small changes

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

---------

Signed-off-by: Althaf66 <althafasharaf02@gmail.com>
Signed-off-by: ALTHAF <114910365+Althaf66@users.noreply.github.com>
Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>
Signed-off-by: Rizul Gupta <112455393+rizul2108@users.noreply.github.com>
Co-authored-by: Althaf66 <althafasharaf02@gmail.com>
Co-authored-by: ALTHAF <114910365+Althaf66@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-22 15:20:57 +02:00
Prasanth Baskar bdb6d985cb
FIX: project delete --force (#437)
* fix: force project delete

- add deletion of immutable rules when using force

Signed-off-by: bupd <bupdprasanth@gmail.com>

* fix lint

Signed-off-by: bupd <bupdprasanth@gmail.com>

---------

Signed-off-by: bupd <bupdprasanth@gmail.com>
2025-04-22 15:04:40 +02:00
Victor Anene f9c570862a
refactor: Config cmd description (#427)
* chore: update config cmd description

Signed-off-by: vickysomtee <vickysomtee@gmail.com>

* Added more info how to use harbor-cli containers correctly

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>
Signed-off-by: vickysomtee <vickysomtee@gmail.com>

* Update README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Patrick Eschenbach <45457307+qcserestipy@users.noreply.github.com>
Signed-off-by: vickysomtee <vickysomtee@gmail.com>

---------

Signed-off-by: vickysomtee <vickysomtee@gmail.com>
Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>
Signed-off-by: Patrick Eschenbach <45457307+qcserestipy@users.noreply.github.com>
Co-authored-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>
Co-authored-by: Patrick Eschenbach <45457307+qcserestipy@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-22 09:30:17 +02:00
Patrick Eschenbach 9055c510d7
Merge pull request #423 from qcserestipy/390_update_readme
Added more info how to use harbor-cli containers correctly
2025-04-17 13:08:20 +02:00
Patrick Eschenbach 18956214f4
Update README.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Patrick Eschenbach <45457307+qcserestipy@users.noreply.github.com>
2025-04-16 20:16:17 +02:00
Patrick Eschenbach 45d3b5ac85
Added more info how to use harbor-cli containers correctly
Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>
2025-04-16 20:09:22 +02:00
Ujjwal Sharma 4c2071f0d0
Updates incorrect config mount location (#416)
Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>
2025-04-16 15:24:38 +02:00
Vadim Bauer ebf8393b08
Artifacthub (#413)
* feat: add metadata labels and creation timestamp for image builds for artifacthub.io

* feat: add license label for image builds

---------

Signed-off-by: Vadim Bauer <vb@container-registry.com>
2025-04-15 17:58:56 +02:00
Vadim Bauer ffc9a8e18f
feat: add metadata labels and creation timestamp for image builds for artifacthub.io (#412) 2025-04-15 17:14:21 +02:00
dependabot[bot] 5cdc50c0c2
build(deps): bump golang.org/x/term from 0.27.0 to 0.31.0 (#380)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.27.0 to 0.31.0.
- [Commits](https://github.com/golang/term/compare/v0.27.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-version: 0.31.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-15 16:36:25 +02:00
Vadim Bauer 5686c8d178 fix: update golangci-lint output format to use --output.tab.path 2025-04-15 16:30:53 +02:00
Vadim Bauer 9abb9ca3ea chore: update dependencies and golangci-lint configuration 2025-04-15 16:25:41 +02:00
Vadim Bauer ac1983e382 chore: update golangci-lint configuration and version 2025-04-15 16:23:55 +02:00
dependabot[bot] 099f441455
build(deps): bump github.com/spf13/cobra from 1.8.1 to 1.9.1 (#334)
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.8.1 to 1.9.1.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.8.1...v1.9.1)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-15 16:07:56 +02:00
dependabot[bot] 39f0af1bb5
build(deps): bump github.com/charmbracelet/bubbletea from 1.2.4 to 1.3.4 (#337)
Bumps [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea) from 1.2.4 to 1.3.4.
- [Release notes](https://github.com/charmbracelet/bubbletea/releases)
- [Changelog](https://github.com/charmbracelet/bubbletea/blob/main/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/bubbletea/compare/v1.2.4...v1.3.4)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/bubbletea
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-15 16:07:50 +02:00
Prasanth Baskar 02199e073b
update dagger and go versions (#409)
Signed-off-by: bupd <bupdprasanth@gmail.com>
2025-04-15 16:06:13 +02:00
dependabot[bot] f51d045bc1
build(deps): bump github.com/spf13/viper from 1.19.0 to 1.20.1 (#369)
Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.19.0 to 1.20.1.
- [Release notes](https://github.com/spf13/viper/releases)
- [Commits](https://github.com/spf13/viper/compare/v1.19.0...v1.20.1)

---
updated-dependencies:
- dependency-name: github.com/spf13/viper
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-15 15:54:38 +02:00
Aurelie Vache cec019adfa
Improve user message in proxy cache project creation without adding registry-id (#395)
* Update create.go

Signed-off-by: Aurelie Vache <scraly@gmail.com>

* fix: remove useless comment

Signed-off-by: scraly <scraly@gmail.com>

* fix: lint

Signed-off-by: scraly <scraly@gmail.com>

---------

Signed-off-by: Aurelie Vache <scraly@gmail.com>
Signed-off-by: scraly <scraly@gmail.com>
2025-04-15 15:52:52 +02:00
Ujjwal Sharma fca4399974
Adds login with trailing slash (#382)
* Adds login with trailing slash

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>

* Remove all trailing slashes

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>

---------

Signed-off-by: Darkhood148 <ujjwal.sharma9999999@gmail.com>
2025-04-15 15:52:02 +02:00
Aurelie Vache 398ee6fb2e
Signed-off-by: scraly <scraly@gmail.com> (#402) 2025-04-15 15:51:07 +02:00
Rizul Gupta 2e4329fc09
fix error loggin in project-list (#404)
Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>
2025-04-15 15:47:32 +02:00
Rizul Gupta 3b47fcc101
feat: replace random widths with standard custom widths (#403)
* add widths

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* change widths

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* change widths

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

---------

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>
2025-04-15 15:46:55 +02:00
Rizul Gupta 09b644f699
Continuing Work from #324: add `projectID` flag to `project delete` cmd (#371)
* add projectID flag option to delete project command

Signed-off-by: adityachopra29 <adityachopra2912@gmail.com>

* remove bool-pointer func

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* fix view func

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* fix delete func

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* fix lint issue

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* fix project view

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

---------

Signed-off-by: adityachopra29 <adityachopra2912@gmail.com>
Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>
Co-authored-by: adityachopra29 <adityachopra2912@gmail.com>
2025-04-15 15:30:24 +02:00
Vadim Bauer 33d95a5ef9
Update golang and dagger (#408)
* checkout first

* chore: update engine and Go version to v0.18.3 and v1.24.2
2025-04-15 15:29:25 +02:00
dependabot[bot] 8184c6f49f
build(deps): bump github.com/charmbracelet/lipgloss from 1.0.0 to 1.1.0 (#353)
Bumps [github.com/charmbracelet/lipgloss](https://github.com/charmbracelet/lipgloss) from 1.0.0 to 1.1.0.
- [Release notes](https://github.com/charmbracelet/lipgloss/releases)
- [Changelog](https://github.com/charmbracelet/lipgloss/blob/master/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/lipgloss/compare/v1.0.0...v1.1.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/lipgloss
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-15 15:04:21 +02:00
Rizul Gupta 716d406a35
Continuing Work from #129: add `immutable` cmd (#374)
* created immutable command

Signed-off-by: ALTHAF <114910365+Althaf66@users.noreply.github.com>

* created immutable list command

Signed-off-by: ALTHAF <althafasharaf02@gmail.com>

* created immutable delete cmd

Signed-off-by: ALTHAF <althafasharaf02@gmail.com>

* move immutable under tag

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* add docs

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* fix lint issues

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

lint errors

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

fix lint issues

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* some minor fixes

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* Update pkg/views/immutable/list/view.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Rizul Gupta <112455393+rizul2108@users.noreply.github.com>

---------

Signed-off-by: ALTHAF <114910365+Althaf66@users.noreply.github.com>
Signed-off-by: ALTHAF <althafasharaf02@gmail.com>
Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>
Signed-off-by: Rizul Gupta <112455393+rizul2108@users.noreply.github.com>
Co-authored-by: ALTHAF <114910365+Althaf66@users.noreply.github.com>
Co-authored-by: ALTHAF <althafasharaf02@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-15 15:04:01 +02:00
Patrick Eschenbach 1d08f33a84
Added environment keyring provider that takes priority over fallback file based keyring provider. This enables containres to store the encryption key more safely as env var or as k8s secret in k8s pods (#387)
Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>
2025-04-10 14:38:45 +02:00
Patrick Eschenbach baa69c6ad5
Add FileKeyring as fallback for secure key storage (#385) 2025-04-09 22:41:05 +02:00
Rizul Gupta e089ed7143
feat: Remove month from autogenerated man-docs (#370)
* remove date from man-docs

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* fix lint error

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

---------

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>
2025-04-08 16:20:28 +02:00
Rizul Gupta 44918b9d4f
feat: Enhance `user list` cmd to fetch all users by default via paginated API calls (#378)
* update-user-list

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* minor fixes in list cmds

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* lint check

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* implement copilot suggestions

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

---------

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>
2025-04-08 16:14:27 +02:00
Patrick Eschenbach 651cfb4090
267 rework cherry pick (#372)
* Added check for existing credentials in present config for login; Skip login view creation if some credentials exist

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added config and data path management description to readme

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added harbor-config docs; Add
ed config behavior to login docs

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added pwd encryption and key ring usage. The encryption key is stored in the keyring with a specified user and service

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Fix: compare existing pwd hash with encrypted pwd

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Fix: Ineffective use of err in view.go

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added interface for keyring provider; added mockKeyRing provider; Added test for encryption functions; Updated config and login tests with mock key ring provider

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Explicitely set mock keyring in sub tests

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Explicitely set mock keyring in sub tests

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added config sub command; added functions for set, get, list and clear config items; added a function to update config on disk

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added error propagation to config sub commands; Added tests for subcommands

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added --name flag for credential selection; Added subcommand tests

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Did sign off rebase and mod tidy

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Explicitely set mock keyring in sub tests

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Explicitely set mock keyring in sub tests

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added config sub command; added functions for set, get, list and clear config items; added a function to update config on disk

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added --name flag for credential selection; Added subcommand tests

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Updated cli auto docs

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Fix bug to add new credentials in case others already exist

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Fix bug to add new credentials in case others already exist; fix bug in config with default config

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Fix for login command with not all flags; Added automatic context switch after login execution and credential update

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Create empty default credentials

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Update: structure login command with ProcessLogin function to deal with login cases; load function searches for existing credentials

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Update: Made function names upper case

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Update: Retrieved upstream docs

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Update: Retrieved upstream docs; added new docs for added commands

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Updated description of config sub commands; added --all flag to config delete subcommand: deletes current credentials and resets current-credential-name

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added tests for config delete --all flag

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Made changes to satisfy golangcli-linter

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* fix: updated config setter command to update verb; adjusted cli docs; changed --all flag to --current flag; adjusted cli docs

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* fix: fixed failing tests

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Rebase and cherry picking

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Readded empty config with place-holders

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Update README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Vadim Bauer <Bauer.vadim@gmail.com>

---------

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>
Signed-off-by: Vadim Bauer <Bauer.vadim@gmail.com>
Co-authored-by: Vadim Bauer <Bauer.vadim@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-08 14:54:36 +02:00
Patrick Eschenbach 4689267fa5
Added logrus timestamp formatter to cli command (#351)
* Added logrus timestamp formatter to cli command

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Updated go version in dagger main and go.mod

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Revoke go version changes

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Moved log formatter to verbose flag conditional

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Update removed login test from tests

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

---------

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>
2025-03-31 18:50:04 +02:00
Prasanth Baskar 33f0f36a0f
remove unwanted changes.patch file (#317)
removing unwanted changes.patch file
which has come with the large prs

Signed-off-by: bupd <bupdprasanth@gmail.com>
2025-03-30 16:25:53 +02:00
Rizul Gupta 801dd8e973
add check for page-size (#345)
* add check for page-size

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* fix lint error

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* add page-size check for all list commands

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* small fix

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* fix lint error

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* fix lint issues

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* fix lint issues

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

---------

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>
Signed-off-by: Rizul Gupta <112455393+rizul2108@users.noreply.github.com>
2025-03-30 16:24:42 +02:00
Rohan Mishra 76100db25b
better authorizaion (#357)
Signed-off-by: Rohan <315scisyb2020rohanmishra@gmail.com>
2025-03-25 15:27:09 +01:00
Rizul Gupta 5f79d02271
fix project search issue (#359)
Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>
2025-03-25 15:26:26 +01:00
Rizul Gupta 97f196196a
default of project list as all projects (#360)
Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>
2025-03-25 15:25:47 +01:00
Rizul Gupta d4e3c6cbec
feat: Remove date and autogenerated line from man docs (#361)
* remove history section from man-docs

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* update file permissions

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

---------

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>
2025-03-25 15:24:41 +01:00
Rizul Gupta c018676326
gen docs for changed files (#363)
Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>
2025-03-25 15:24:14 +01:00
Rizul Gupta f31f1b7334
feat: Add `status` command (#348)
* add status commond

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* remove authAdmin

Co-authored-by: Prasanth Baskar <bupdprasanth@gmail.com>
Signed-off-by: Rizul Gupta <112455393+rizul2108@users.noreply.github.com>
Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* make long as default

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* requested changes done

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* add documentation

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* modify documentation

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* fix lint issues

-M

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

---------

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>
Signed-off-by: Rizul Gupta <112455393+rizul2108@users.noreply.github.com>
Co-authored-by: Prasanth Baskar <bupdprasanth@gmail.com>
2025-03-25 15:05:08 +01:00
Rizul Gupta 2e4a4f4140
table doesn't get printed twice (#347)
Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>
2025-03-18 15:16:33 +01:00
Prasanth Baskar f6fd361644
move dagger to .dagger (#344) 2025-03-11 21:22:54 +01:00
Rizul Gupta 3cff25fbb0
project create cmd fix (#307)
Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>
2025-03-11 15:09:59 +01:00
Rizul Gupta 524adda2ca
enforce exact argument count for cmds (#302)
* enforce exact argument count for cmds

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* fix: enforce exact argument count for create project command

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* revert change for project create cmd

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

* lint error fix

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>

---------

Signed-off-by: Rizul Gupta <mail2rizul@gmail.com>
2025-03-11 15:08:32 +01:00
Prasanth Baskar db9d24ea9b
feat: Improve Performance of ` delete` Command with Concurrency (#80)
* add: batch deletion of resources

handles concurrent deletion of projects, registries and users

Signed-off-by: bupd <bupdprasanth@gmail.com>

* update deletion events

Signed-off-by: bupd <bupdprasanth@gmail.com>

---------

Signed-off-by: bupd <bupdprasanth@gmail.com>
2025-03-11 15:04:42 +01:00
Krishna Madhwani bcc6fe16e8
parser-fix (#299)
Signed-off-by: Roaster05 <krishnamadhwani2@gmail.com>
2025-03-10 14:17:37 +01:00
Vadim Bauer 0f13e438b2
docs: reposition the FOSSA badge (#339)
docs: reposition the FOSSA badge

Signed-off-by: Vadim Bauer <vb@container-registry.com>
2025-03-06 09:05:50 +01:00
fossabot 0a17861f33
Add license scan report and status (#338)
Signed off by: fossabot <badges@fossa.com>
2025-03-06 09:03:37 +01:00
Prasanth Baskar c702fe9c33
fix release issue (#335)
Signed-off-by: bupd <bupdprasanth@gmail.com>
2025-02-19 18:19:19 +01:00
479 changed files with 27793 additions and 1344 deletions

View File

@ -2,3 +2,4 @@
/internal/dagger
/internal/querybuilder
/internal/telemetry
/.env

101
.dagger/README.md Normal file
View File

@ -0,0 +1,101 @@
# 🛠️ Harbor CLI Dagger Pipeline
We use [Dagger](https://dagger.io) to define a CI/CD pipeline for building, linting, and publishing the [Harbor CLI](https://github.com/goharbor/harbor-cli).
This README will help beginners understand how to use Dagger in local development and CI workflows.
## Prerequisites
Before you start, ensure you have the following:
1. Dagger: Install the latest version of Dagger. You can check the official documentation for installation steps: [Dagger Installation Guide](https://docs.dagger.io/install).
## Dagger Setup and Development Mode
### Run Dagger Develop
```bash
dagger develop
```
This command will generate the necessary files and configuration for building and running Dagger.
## 📦 Dagger Functions Explained
### 🔧 `BuildDev(platform)`
Builds a development binary for your target platform.
```bash
dagger call build-dev --platform="linux/amd64" export --path=bin/harbor-dev
```
### 🧼 `LintReport()`
Runs `golangci-lint` on your code and saves the report to a file.
```bash
dagger call lint-report export --path=./LintReport.json
```
### 📝 `TestCoverageReport()`
Runs go test coverage tools and creates a report.
```bash
dagger call test-coverage-report export --path=coverage-report.md
```
### ✅ `CheckCoverageThreshold(context, threshold)`
Runs go test coverage tools and creates a report. The total coverage is compared to a threshold that can be set to e.g. 80%.
```bash
dagger call check-coverage-threshold --threshold 80.0
```
### 🚀 `PublishImage(registry, imageTags)`
Builds and publishes the Harbor CLI image to the given container registry with proper OCI metadata labels.
Before running the command you have to export you registry password
```shell
export REGPASS=Harbor12345
```
```bash
dagger call publish-image \
--registry=demo.goharbor.io \
--registry-username=harbor-cli \
--registry-password=env:REGPASS \
--imageTags=v0.1.0,latest
```
---
## ⚙️ Configuration Constants
Dagger uses these constant versions (you can modify them as needed):
```go
const (
GO_VERSION = "1.24.2"
GOLANGCILINT_VERSION = "v2.1.2"
SYFT_VERSION = "v1.9.0"
GORELEASER_VERSION = "v2.3.2"
)
```
---
## 💡 Tips for Beginners
- Every container step is **reproducible** you can build locally or in GitHub Actions without changes.
- Use Dagger to cache Go builds and lint output, speeding up re-runs.
---
## 📚 References
- [Dagger Go SDK Docs](https://pkg.go.dev/dagger.io/dagger)
- [golangci-lint](https://golangci-lint.run/)
- [Goreleaser](https://goreleaser.com/)

50
.dagger/go.mod Normal file
View File

@ -0,0 +1,50 @@
module dagger/harbor-cli
go 1.24.4
require (
github.com/99designs/gqlgen v0.17.74
github.com/Khan/genqlient v0.8.1
github.com/vektah/gqlparser/v2 v2.5.27
go.opentelemetry.io/otel v1.35.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0
go.opentelemetry.io/otel/log v0.8.0
go.opentelemetry.io/otel/sdk v1.35.0
go.opentelemetry.io/otel/sdk/log v0.8.0
go.opentelemetry.io/otel/trace v1.35.0
go.opentelemetry.io/proto/otlp v1.3.1
golang.org/x/sync v0.15.0
google.golang.org/grpc v1.73.0
)
require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 // indirect
github.com/sosodev/duration v1.3.1 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0
go.opentelemetry.io/otel/sdk/metric v1.35.0
golang.org/x/net v0.41.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
google.golang.org/protobuf v1.36.6 // indirect
)
replace go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc => go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0
replace go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp => go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0
replace go.opentelemetry.io/otel/log => go.opentelemetry.io/otel/log v0.8.0
replace go.opentelemetry.io/otel/sdk/log => go.opentelemetry.io/otel/sdk/log v0.8.0

85
.dagger/go.sum Normal file
View File

@ -0,0 +1,85 @@
github.com/99designs/gqlgen v0.17.74 h1:1FuVtkXxOc87xpKio3f6sohREmec+Jvy86PcYOuwgWo=
github.com/99designs/gqlgen v0.17.74/go.mod h1:a+iR6mfRLNRp++kDpooFHiPWYiWX3Yu1BIilQRHgh10=
github.com/Khan/genqlient v0.8.1 h1:wtOCc8N9rNynRLXN3k3CnfzheCUNKBcvXmVv5zt6WCs=
github.com/Khan/genqlient v0.8.1/go.mod h1:R2G6DzjBvCbhjsEajfRjbWdVglSH/73kSivC9TLWVjU=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN9coASF1GusYX6AlU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0/go.mod h1:igFoXX2ELCW06bol23DWPB5BEWfZISOzSP5K2sbLea0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
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/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=
github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0/go.mod h1:5KXybFvPGds3QinJWQT7pmXf+TN5YIa7CNYObWRkj50=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 h1:9kV11HXBHZAvuPUZxmMWrH8hZn/6UnHX4K0mu36vNsU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0/go.mod h1:JyA0FHXe22E1NeNiHmVp7kFHglnexDQ7uRWDiiJ1hKQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI=
go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk=
go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs=
go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -23,10 +23,9 @@ import (
)
const (
GOLANGCILINT_VERSION = "v1.61.0"
GO_VERSION = "1.22.5"
SYFT_VERSION = "v1.9.0"
GORELEASER_VERSION = "v2.3.2"
GOLANGCILINT_VERSION = "v2.1.2"
GO_VERSION = "1.24.4"
GORELEASER_VERSION = "v2.8.2"
)
func New(
@ -139,7 +138,7 @@ func (m *HarborCli) LintReport(ctx context.Context) *dagger.File {
report := "golangci-lint.report"
return m.lint(ctx).WithExec([]string{
"golangci-lint", "run", "-v",
"--out-format", "github-actions:" + report,
"--output.tab.path=" + report,
"--issues-exit-code", "0",
}).File(report)
}
@ -181,6 +180,9 @@ func (m *HarborCli) PublishImage(
}
fmt.Printf("provided tags: %s\n", imageTags)
// Get current time for image creation timestamp
creationTime := time.Now().UTC().Format(time.RFC3339)
for _, builder := range builders {
os, _ := builder.EnvVariable(ctx, "GOOS")
arch, _ := builder.EnvVariable(ctx, "GOARCH")
@ -195,6 +197,13 @@ func (m *HarborCli) PublishImage(
WithFile("/harbor", builder.File("./harbor")).
WithExec([]string{"ls", "-al"}).
WithExec([]string{"./harbor", "version"}).
// Add required metadata labels for ArtifactHub
WithLabel("org.opencontainers.image.created", creationTime).
WithLabel("org.opencontainers.image.description", "Harbor CLI - A command-line interface for CNCF Harbor, the cloud native registry!").
WithLabel("io.artifacthub.package.readme-url", "https://raw.githubusercontent.com/goharbor/harbor-cli/main/README.md").
WithLabel("org.opencontainers.image.source", "https://github.com/goharbor/harbor-cli").
WithLabel("org.opencontainers.image.version", version).
WithLabel("io.artifacthub.package.license", "Apache-2.0").
WithEntrypoint([]string{"/harbor"})
releaseImages = append(releaseImages, ctr)
}
@ -218,7 +227,7 @@ func (m *HarborCli) PublishImage(
// SnapshotRelease Create snapshot non OCI artifacts with goreleaser
func (m *HarborCli) SnapshotRelease(ctx context.Context) *dagger.Directory {
return m.goreleaserContainer().
WithExec([]string{"goreleaser", "release", "--snapshot", "--clean", "--skip", "validate"}).
WithExec([]string{"goreleaser", "release", "--snapshot", "--clean"}).
Directory("/src/dist")
}
@ -227,32 +236,28 @@ func (m *HarborCli) Release(ctx context.Context, githubToken *dagger.Secret) {
goreleaser := m.goreleaserContainer().
WithSecretVariable("GITHUB_TOKEN", githubToken).
WithExec([]string{"goreleaser", "release", "--clean"})
_, err := goreleaser.Stderr(ctx)
error, err := goreleaser.Stderr(ctx)
if err != nil {
log.Printf("Error occured during release: %s", err)
return
}
if len(error) > 0 {
log.Printf("Error occured while release: %s", err)
return
}
log.Println("Release tasks completed successfully 🎉")
}
// Return a container with the goreleaser binary mounted and the source directory mounted.
func (m *HarborCli) goreleaserContainer() *dagger.Container {
// Export the syft binary from the syft container as a file to generate SBOM
syft := dag.Container().
From(fmt.Sprintf("anchore/syft:%s", SYFT_VERSION)).
WithMountedCache("/go/pkg/mod", dag.CacheVolume("syft-gomod")).
File("/syft")
return dag.Container().
From(fmt.Sprintf("goreleaser/goreleaser:%s", GORELEASER_VERSION)).
WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+GO_VERSION)).
WithEnvVariable("GOMODCACHE", "/go/pkg/mod").
WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+GO_VERSION)).
WithEnvVariable("GOCACHE", "/go/build-cache").
WithFile("/bin/syft", syft).
WithMountedDirectory("/src", m.Source).
WithWorkdir("/src").
WithEnvVariable("TINI_SUBREAPER", "true")
WithWorkdir("/src")
}
// Generate CLI Documentation and return the directory containing the generated files
@ -285,6 +290,7 @@ func (m *HarborCli) Test(ctx context.Context) (string, error) {
}
// Executes Go tests and returns TestReport in json file
// TestReport executes Go tests and returns only the JSON report file
func (m *HarborCli) TestReport(ctx context.Context) *dagger.File {
reportName := "TestReport.json"
test := dag.Container().
@ -296,11 +302,93 @@ func (m *HarborCli) TestReport(ctx context.Context) *dagger.File {
WithExec([]string{"go", "install", "gotest.tools/gotestsum@latest"}).
WithMountedDirectory("/src", m.Source).
WithWorkdir("/src").
WithExec([]string{"gotestsum", "--jsonfile", reportName})
WithExec([]string{"gotestsum", "--jsonfile", reportName, "./..."})
return test.File(reportName)
}
func (m *HarborCli) TestCoverage(ctx context.Context) *dagger.File {
coverage := "coverage.out"
test := dag.Container().
From("golang:"+GO_VERSION+"-alpine").
WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+GO_VERSION)).
WithEnvVariable("GOMODCACHE", "/go/pkg/mod").
WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+GO_VERSION)).
WithEnvVariable("GOCACHE", "/go/build-cache").
WithExec([]string{"go", "install", "gotest.tools/gotestsum@latest"}).
WithMountedDirectory("/src", m.Source).
WithWorkdir("/src").
WithExec([]string{"gotestsum", "--", "-coverprofile=" + coverage, "./..."})
return test.File(coverage)
}
// TestCoverageReport processes coverage data and returns a formatted markdown report
func (m *HarborCli) TestCoverageReport(ctx context.Context) *dagger.File {
coverageFile := "coverage.out"
reportFile := "coverage-report.md"
test := dag.Container().
From("golang:"+GO_VERSION+"-alpine").
WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+GO_VERSION)).
WithEnvVariable("GOMODCACHE", "/go/pkg/mod").
WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+GO_VERSION)).
WithEnvVariable("GOCACHE", "/go/build-cache").
WithMountedDirectory("/src", m.Source).
WithWorkdir("/src").
WithExec([]string{"apk", "add", "--no-cache", "bc"}).
WithExec([]string{"go", "test", "-coverprofile=" + coverageFile, "./..."})
return test.WithExec([]string{"sh", "-c", `
echo "<h2> 📊 Test Coverage Results</h2>" > ` + reportFile + `
if [ ! -f "` + coverageFile + `" ]; then
echo "<p>❌ Coverage file not found!</p>" >> ` + reportFile + `
exit 1
fi
total_coverage=$(go tool cover -func=` + coverageFile + ` | grep total: | grep -Eo '[0-9]+\.[0-9]+')
echo "DEBUG: Total coverage is $total_coverage" >&2
if (( $(echo "$total_coverage >= 80.0" | bc -l) )); then
emoji="✅"
elif (( $(echo "$total_coverage >= 60.0" | bc -l) )); then
emoji="⚠️"
else
emoji="❌"
fi
echo "<p><b>Total coverage: $emoji $total_coverage% (Target: 80%)</b></p>" >> ` + reportFile + `
echo "<details><summary>Detailed package coverage</summary><pre>" >> ` + reportFile + `
go tool cover -func=` + coverageFile + ` >> ` + reportFile + `
echo "</pre></details>" >> ` + reportFile + `
cat ` + reportFile + ` >&2
`}).File(reportFile)
}
// Checks for vulnerabilities using govulncheck
func (m *HarborCli) vulnerabilityCheck(ctx context.Context) *dagger.Container {
return dag.Container().
From("golang:"+GO_VERSION+"-alpine").
WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+GO_VERSION)).
WithEnvVariable("GOMODCACHE", "/go/pkg/mod").
WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+GO_VERSION)).
WithEnvVariable("GOCACHE", "/go/build-cache").
WithExec([]string{"go", "install", "golang.org/x/vuln/cmd/govulncheck@latest"}).
WithMountedDirectory("/src", m.Source).
WithWorkdir("/src")
}
// Runs a vulnerability check using govulncheck
func (m *HarborCli) VulnerabilityCheck(ctx context.Context) (string, error) {
return m.vulnerabilityCheck(ctx).
WithExec([]string{"govulncheck", "-show", "verbose", "./..."}).
Stderr(ctx)
}
// Runs a vulnerability check using govulncheck and writes results to vulnerability-check.report
func (m *HarborCli) VulnerabilityCheckReport(ctx context.Context) *dagger.File {
report := "vulnerability-check.report"
return m.vulnerabilityCheck(ctx).
WithExec([]string{
"sh", "-c", fmt.Sprintf("govulncheck ./... > %s", report),
}).File(report)
}
// Parse the platform string into os and arch
func parsePlatform(platform string) (string, string, error) {
parts := strings.Split(platform, "/")

View File

@ -31,17 +31,17 @@ jobs:
- name: Check for changes
run: |
# Check if any newly added docs exist
untracked_files=$(git ls-files --others --exclude-standard)
# Check if any docs have been modified
changed_files=$(git ls-files --others --modified --deleted --exclude-standard)
# If there are untracked files, fail the workflow
if [ -n "$untracked_files" ]; then
echo "New Untracked files found"
echo "please check if docs were added for new commands"
echo "$untracked_files"
# If there are files changed, fail the workflow
if [ -n "$changed_files" ]; then
echo "file changes found"
echo "please check if docs were added for new commands or updated for new commands"
echo "$changed_files"
exit 1 # This will fail the workflow
else
echo "No untracked files found."
echo "No file changes found."
fi
continue-on-error: false
@ -70,6 +70,53 @@ jobs:
# run: |
# reviewdog -f=sarif -name="Golang Linter Report" -reporter=github-check -filter-mode nofilter -fail-level any -tee < golangci-lint-report.sarif
vulnerability-check:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Dagger Version
uses: sagikazarmark/dagger-version-action@v0.0.1
- name: Run Vulnerability Check
uses: dagger/dagger-for-github@v7
with:
version: ${{ steps.dagger_version.outputs.version }}
verb: call
args: vulnerability-check-report export --path=vulnerability-check.report
- name: Generate vulnerability summary
run: |
echo "<h2> 🔒 Vulnerability Check Results</h2>" >> $GITHUB_STEP_SUMMARY
cat vulnerability-check.report >> $GITHUB_STEP_SUMMARY
# Check if the lint report contains any content (error or issues)
if ! grep -q "No vulnerabilities found." vulnerability-check.report; then
# If the file contains content, output an error message and exit with code 1
echo "⚠️ Linting issues found!" >> $GITHUB_STEP_SUMMARY
exit 1
fi
test-release:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Dagger Version
uses: sagikazarmark/dagger-version-action@v0.0.1
- name: Test Release
uses: dagger/dagger-for-github@v7
with:
version: ${{ steps.dagger_version.outputs.version }}
verb: call
args: snapshot-release
test-code:
runs-on: ubuntu-latest
steps:
@ -90,6 +137,33 @@ jobs:
with:
fromJSONFile: TestReport.json
- name: Run Test Coverage Report
if: github.event_name == 'pull_request'
uses: dagger/dagger-for-github@v7
with:
version: ${{ steps.dagger_version.outputs.version }}
verb: call
args: test-coverage-report export --path=coverage-report.md
- name: Add coverage to step summary
if: github.event_name == 'pull_request'
run: cat coverage-report.md >> $GITHUB_STEP_SUMMARY
- name: Run Test Coverage
if: github.event_name == 'pull_request'
uses: dagger/dagger-for-github@v7
with:
version: ${{ steps.dagger_version.outputs.version }}
verb: call
args: test-coverage export --path=coverage.out
- uses: codecov/codecov-action@v5
if: github.event_name == 'pull_request'
with:
verbose: true
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
- name: Build Binary
uses: dagger/dagger-for-github@v7
with:
@ -138,9 +212,17 @@ jobs:
runs-on: ubuntu-latest
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/'))
steps:
- name: Print GitHub ref for debugging
run: |
echo "GitHub ref: $GITHUB_REF"
- name: Checkout repo
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Checkout repo
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/'))
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Push images
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/'))
@ -152,12 +234,6 @@ jobs:
REGISTRY_ADDRESS: ${{ vars.REGISTRY_ADDRESS }}
REGISTRY_USERNAME: ${{ vars.REGISTRY_USERNAME }}
- name: Checkout repo
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/'))
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Create Release
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/'))
uses: dagger/dagger-for-github@v7

View File

@ -1,44 +1,47 @@
run:
timeout: 3m
linters-settings:
gofmt:
# Simplify code: gofmt with `-s` option.
# Default: true
simplify: false
misspell:
locale: US,UK
stylecheck:
checks: [
"ST1019", # Importing the same package multiple times.
]
goheader:
template-path: copyright.tmpl
version: "2"
linters:
enable:
# Default linters are already enabled, these are the additional ones
- gosimple
- typecheck
- gofmt
- goimports
- gosec
- nilnil
- unused
- errcheck
- staticcheck
- bodyclose
- dupl
- goheader
- gosec
- misspell
- ineffassign
- nilnil
- staticcheck
- whitespace
- bodyclose
- govet
- stylecheck
# - wrapcheck
# - gocritic
# - revive #, enable once current issues are resolved
issues:
exclude-dirs:
- dagger/internal
exclude-files:
- ^.*\\.gen\\.go$
settings:
goheader:
template-path: copyright.tmpl
misspell:
locale: US,UK
staticcheck:
checks:
- ST1019
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- ^.*\\.gen\\.go$
- dagger/internal
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
- goimports
settings:
gofmt:
simplify: false
exclusions:
generated: lax
paths:
- ^.*\\.gen\\.go$
- dagger/internal
- third_party$
- builtin$
- examples$

View File

@ -1,5 +1,5 @@
version: 2
project_name: harbor
project_name: harbor-cli
before:
hooks:
@ -11,6 +11,7 @@ builds:
- CGO_ENABLED=0
ldflags:
- -w -s -X github.com/goharbor/harbor-cli/cmd/harbor/internal/version.GitCommit={{.FullCommit}}
- -X github.com/goharbor/harbor-cli/cmd/harbor/internal/version.Version={{.Tag}}
goos:
- linux
- windows
@ -62,6 +63,22 @@ release:
owner: goharbor # Your GitHub repository owner
name: harbor-cli # Your GitHub repository name
# https://goreleaser.com/customization/homebrew/
brews:
- repository:
owner: goharbor # GitHub user/org who owns the tap repo
name: homebrew-tap # Tap repo name (i.e., goharbor/homebrew-tap)
branch: main
name: harbor-cli # Name of the CLI, becomes harbor-cli.rb
commit_author: # Who commits to the tap repo
name: goreleaserbot
email: bot@goreleaser.com
commit_msg_template: "Brew formula update for {{ .ProjectName }} version {{ .Tag }}"
homepage: "https://goharbor.io"
description: "Harbor CLI for interacting with Harbor registry" # Formula description
test: |
system "#{bin}/harbor-cli", "version" # Formula test (after install)
changelog:
use: github
filters:

View File

@ -0,0 +1,141 @@
# Contributing to Harbor CLI
Thank you for your interest in contributing to the Harbor CLI project!
We welcome contributions of all kinds, from bug fixes and documentation improvements to new features and suggestions.
## Overview
The **Harbor CLI** is a powerful command-line tool to interact with the [Harbor container registry](https://goharbor.io/). It's built in Go and helps users manage Harbor resources like projects, registries, artifacts, and more — directly from their terminal.
## Getting Started
### Run using Container
You can try the CLI immediately using Docker:
```bash
docker run -ti --rm -v $HOME/.harbor/config.yaml:/root/.harbor/config.yaml registry.goharbor.io/harbor-cli/harbor-cli --help
```
### Alias (Optional)
```bash
echo "alias harbor='docker run -ti --rm -v \$HOME/.harbor/config.yaml:/root/.harbor/config.yaml registry.goharbor.io/harbor-cli/harbor-cli'" >> ~/.zshrc
source ~/.zshrc
```
### Build from Source
Make sure [Go](https://go.dev/) is installed (≥ v1.20).
```bash
git clone https://github.com/goharbor/harbor-cli.git && cd harbor-cli
go build -o harbor-cli cmd/harbor/main.go
./harbor-cli --help
```
Alternatively, use [Dagger](https://docs.dagger.io/) for isolated builds:
```bash
dagger call build-dev --platform darwin/arm64 export --path=./harbor-cli
./harbor-dev --help
```
## Project Structure
```
..
├── cmd/harbor/ # Entry point (main.go) and all CLI commands (Cobra-based)
├── pkg/ # Shared utilities and internal packages used across commands
├── docs/ # Project documentation
├── test/ # CLI tests and test data
├── .github/ # GitHub workflows and issue templates
├── go.mod / go.sum # Go module dependencies
└── README.md # Project overview and usage
```
## How to Contribute
### 1. [Fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) and Clone
```bash
git clone https://github.com/your-username/harbor-cli.git
cd harbor-cli
```
### 2. Create Your Feature Branch
```bash
git checkout -b feat/<your-feature-name>
```
### 3. Make Your Changes
Follow coding and formatting guidelines.
### 4. Test Locally
Ensure your changes work as expected.
```bash
gofmt -s -w .
dagger call build-dev --platform darwin/arm64 export --path=./harbor-cli #Recommended
./harbor-dev --help
```
If dagger is not installed in your system, you can also build the project using the following commands:
```bash
gofmt -s -w .
go build -o ./bin/harbor-cli cmd/harbor/main.go
./bin/harbor-cli --help
```
### 5. Commit with a clear message
```bash
git commit -s -m "feat(project): add delete command for project resources"
```
### 6. Push and Open a PR
```bash
git push origin feat/<your-feature-name>
```
Then, [Open a Pull Request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request) on GitHub
## 🧪 Running Tests
> ✅ Note: Add your CLI or unit tests to the `test/` directory.
```bash
go test ./...
```
## 🧹 Code Guidelines
- Use `go fmt ./...` to format your code.
- Use descriptive commit messages:
- `feat`: New feature
- `fix`: Bug fix
- `docs`: Documentation only
- `test`: Adding or updating tests
- `refactor`: Code cleanup
- `chore`: Maintenance tasks
## 📬 Communication
- **Slack:** Join us in [#harbor-cli](https://cloud-native.slack.com/messages/harbor-cli/)
- **Issues:** Use [GitHub Issues](https://github.com/goharbor/harbor-cli/issues) for bugs, ideas, or questions.
- **Mailing List:**
- Users: [harbor-users@lists.cncf.io](https://lists.cncf.io/g/harbor-users)
- Devs: [harbor-dev@lists.cncf.io](https://lists.cncf.io/g/harbor-dev)
## 📄 License
All contributions are under the [Apache 2.0 License](./LICENSE).
---
**Thank you for contributing to Harbor CLI! Your work helps improve the Harbor ecosystem for everyone. 🙌**

143
README.md
View File

@ -1,37 +1,76 @@
![harbor-3](https://github.com/goharbor/harbor-cli/assets/70086051/835ab686-1cce-4ac7-bc57-05a35c2b73cc)
![Harbor-CLI Logo_256px](https://github.com/user-attachments/assets/fa18e8f0-a2e4-4462-ab2d-446a88f9edb3)
**Welcome to the Harbor CLI project! This powerful command-line tool facilitates seamless interaction with the Harbor container registry. It simplifies various tasks such as creating, updating, and managing projects, registries, and other resources in Harbor.**
**Harbor CLI — a command-line interface for interacting with your Harbor container registry. A streamlined, user-friendly alternative to the WebUI, as your daily driver or for scripting and automation.**
[![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/harbor-cli)](https://artifacthub.io/packages/search?repo=harbor-cli)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fgoharbor%2Fharbor-cli.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fgoharbor%2Fharbor-cli?ref=badge_shield)
[![codecov](https://codecov.io/gh/goharbor/harbor-cli/branch/main/graph/badge.svg)](https://codecov.io/gh/goharbor/harbor-cli)
[![Go Report Card](https://goreportcard.com/badge/github.com/goharbor/harbor-cli)](https://goreportcard.com/report/github.com/goharbor/harbor-cli)
# Project Scope 🧪
# Scope 🧪
The Harbor CLI is designed to enhance your interaction with the Harbor container registry. Built on Golang, it offers a user-friendly interface to perform various tasks related to projects, registries, and more. Whether you're creating, updating, or managing resources, the Harbor CLI streamlines your workflow efficiently.
1. CLI alternative to the WebUI
2. Tool for scripting and automation of common repeatable Harbor tasks running on your machine or inside your pipeline
# Project Features 🤯
# Features
The project's first goal is to reach WebUI parity.
🔹 Get details about projects, registries, repositories and more <br>
🔹 Create new projects, registries, and other resources <br>
🔹 Delete projects, registries, and other resources <br>
🔹 Run commands with various flags for enhanced functionality <br>
🔹 More features coming soon... 🚧
```shell
✅ project Mange projects
✅ repo Manage repositories
✅ artifact Manage artifacts
✅ label Manage labels
✅ tag Manage tags
✅ quota Manage quotas
✅ webhook Manage webhook policies
✅ robot Robot Account
✅ login Log in to Harbor registry
✅ user Manage users
✅ registry Manage registries
❌ replication Manage replication
✅ config Manage the config of the Harbor CLI
✅ cve-allowlist Manage system CVE allowlist
✅ health Get the health status of Harbor components
✅ instance Manage preheat provider instances in Harbor
✅ info Display detailed Harbor system, statistics, and CLI environment information
✅ scanner scanner commands
✅ schedule Schedule jobs in Harbor
✅ completion Generate the autocompletion script for the specified shell\
✅ help Help about any command
✅ version Version of Harbor CLI
```
# Installation
## Container
It is straightforward to use the Harbor CLI as a container. You can run the following command to use the Harbor CLI as a container:
Running Harbor CLI as a container is simple. Use the following command to get started:
```shell
docker run -ti --rm -v $HOME/.harbor/config.yaml:/root/.harbor/config.yaml registry.goharbor.io/harbor-cli/harbor-cli --help
docker run -ti --rm -v $HOME/.config/harbor-cli/config.yaml:/root/.config/harbor-cli/config.yaml \
-e HARBOR_ENCRYPTION_KEY=$(echo "ThisIsAVeryLongPassword" | base64) \
registry.goharbor.io/harbor-cli/harbor-cli \
--help
```
Use the `HARBOR_ENCRYPTION_KEY` container environment variable as a base64-encoded 32-byte key for AES-256 encryption. This securely stores your harbor login password.
If you intend
to run the CLI as a container,it is advised
to set the following environment variables and to create an alias
and append the alias to your .zshrc or .bashrc file
# Add the following command to create an alias and append the alias to your .zshrc or .bashrc file
```shell
echo "alias harbor='docker run -ti --rm -v \$HOME/.harbor/config.yaml:/root/.harbor/config.yaml registry.goharbor.io/harbor-cli/harbor-cli'" >> ~/.zshrc
echo "export HARBOR_CLI_CONFIG=\$HOME/.config/harbor-cli/config.yaml" >> ~/.zshrc
echo "export HARBOR_ENCRYPTION_KEY=\$(cat <path_to_32bit_private_key_file> | base64)" >> ~/.zshrc
echo "alias harbor='docker run -ti --rm -v \$HARBOR_CLI_CONFIG:/root/.config/harbor-cli/config.yaml -e HARBOR_ENCRYPTION_KEY=\$HARBOR_ENCRYPTION_KEY registry.goharbor.io/harbor-cli/harbor-cli'" >> ~/.zshrc
source ~/.zshrc # or restart your terminal
```
@ -40,14 +79,14 @@ source ~/.zshrc # or restart your terminal
Harbor CLI will soon be published on Homebrew.
Meantime, we recommend using Harbor in the Container
or download the binary from the [releases page](https://github.com/goharbor/harbor-cli/releases)
or downloading the binary from the [releases page](https://github.com/goharbor/harbor-cli/releases)
## Add the Harbor CLI to your Container Image
Using Curl or Wget isn't recommended
for adding the Harbor CLI to your container.
Using Curl or Wget isn't needed if you want to
add the Harbor CLI to your container.
Instead, we recommend copying the Harbor CLI from our official image
by using the following Dockerfile:
@ -78,19 +117,26 @@ harbor help
Available Commands:
artifact Manage artifacts
completion Generate the autocompletion script for the specified shell
health Get the health status of Harbor components
help Help about any command
login Log in to Harbor registry
project Manage projects and assign resources to them
registry Manage registries
repo Manage repositories
user Manage users
version Version of Harbor CLI
artifact Manage artifacts
completion Generate the autocompletion script for the specified shell
config Manage the config of the Harbor CLI
cve-allowlist Manage system CVE allowlist
health Get the health status of Harbor components
help Help about any command
info Show the current credential information
instance Manage preheat provider instances in Harbor
label Manage labels in Harbor
login Log in to Harbor registry
project Manage projects and assign resources to them
registry Manage registries
repo Manage repositories
schedule Schedule jobs in Harbor
tag Manage tags in Harbor registry
user Manage users
version Version of Harbor CLI
Flags:
--config string config file (default is $HOME/.harbor/config.yaml) (default "/Users/vadim/.harbor/config.yaml")
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
-h, --help help for harbor
-o, --output-format string Output format. One of: json|yaml
-v, --verbose verbose output
@ -100,8 +146,36 @@ Use "harbor [command] --help" for more information about a command.
```
#### Config Management
##### Hierarchy
Use the `--config` flag to specify a custom configuration file path (the highest priority).
```bash
harbor --config /path/to/custom/config.yaml artifact list
```
If `--config` is not provided, Harbor CLI checks the `HARBOR_CLI_CONFIG` environment variable for the config file path.
```bash
export HARBOR_CLI_CONFIG=/path/to/custom/config.yaml
harbor artifact list
```
If neither is set, it defaults to `$XDG_CONFIG_HOME/harbor-cli/config.yaml` or `$HOME/.config/harbor-cli/config.yaml` if `XDG_CONFIG_HOME` is unset.
```bash
harbor artifact list
```
##### Data Path
- Data paths are determined by the `XDG_DATA_HOME` environment variable.
- If `XDG_DATA_HOME` is not set, it defaults to `$HOME/.local/share/harbor-cli/data.yaml`.
- The data file always contains the path of the latest config used.
##### Config TL;DR
- `--config` flag > `HARBOR_CLI_CONFIG` environment variable > default XDG config paths.
- Environment variables override default settings, and the `--config` flag takes precedence over both environment variables and defaults.
- The data file always contains the path of the latest config used.
#### Log in to Harbor Registry
@ -154,9 +228,10 @@ Windows | ✅
# Build From Source
Make sure you have latest [Dagger](https://docs.dagger.io/) installed in your system.
Make sure you have the latest [Dagger](https://docs.dagger.io/) installed in your system.
#### Using Dagger
```bash
git clone https://github.com/goharbor/harbor-cli.git && cd harbor-cli
dagger call build-dev --platform darwin/arm64 export --path=./harbor-cli
@ -172,8 +247,8 @@ go build -o harbor-cli cmd/harbor/main.go
# Version Compatibility With Harbor
At the moment, the Harbor CLI is developed and tested with Harbor 2.11.
The CLI should work with versions prior to 2.11,
At the moment, the Harbor CLI is developed and tested with Harbor 2.13.
The CLI should work with versions prior to 2.13,
but not all functionalities may be available or work as expected.
Harbor <2.0.0 is not supported.
@ -191,6 +266,9 @@ Harbor <2.0.0 is not supported.
This project is licensed under the Apache 2.0 License. See the [LICENSE](https://github.com/goharbor/harbor-cli/blob/main/LICENSE) file for details.
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fgoharbor%2Fharbor-cli.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fgoharbor%2Fharbor-cli?ref=badge_large)
# Acknowledgements
This project is maintained by the Harbor community. We thank all our contributors and users for their support.
@ -199,3 +277,4 @@ This project is maintained by the Harbor community. We thank all our contributor
For any questions or issues, please open an issue on our [GitHub Issues](https://github.com/goharbor/harbor-cli/issues) page.<br>
Give a ⭐ if this project helped you, Thank YOU!

View File

@ -1,79 +0,0 @@
diff --git a/.golangci.yaml b/.golangci.yaml
index b9a4d4f27..fb281a936 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -1,12 +1,32 @@
run:
timeout: 3m
+linters-settings:
+ gofmt:
+ # Simplify code: gofmt with `-s` option.
+ # Default: true
+ simplify: false
+ misspell:
+ locale: US,UK
+ stylecheck:
+ checks: [
+ "ST1019", # Importing the same package multiple times.
+ ]
+ goheader:
+ template-path: copyright.tmpl
+
linters:
enable:
# Default linters are already enabled, these are the additional ones
+ - gosimple
- typecheck
- gofmt
+ - goimports
- gosec
- nilnil
+ - unused
+ - errcheck
+ - staticcheck
+ - dupl
# - wrapcheck
# - gocritic
# - revive #, enable once current issues are resolved
diff --git a/pkg/views/artifact/list/view.go b/pkg/views/artifact/list/view.go
index 3b851aeeb..62901de74 100644
--- a/pkg/views/artifact/list/view.go
+++ b/pkg/views/artifact/list/view.go
@@ -2,13 +2,14 @@ package list
import (
"fmt"
+ "os"
+ "strconv"
+
"github.com/charmbracelet/bubbles/table"
tea "github.com/charmbracelet/bubbletea"
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/base/tablelist"
- "os"
- "strconv"
)
var columns = []table.Column{
diff --git a/pkg/views/artifact/view/view.go b/pkg/views/artifact/view/view.go
index 0ab46ae7f..13288d17b 100644
--- a/pkg/views/artifact/view/view.go
+++ b/pkg/views/artifact/view/view.go
@@ -2,13 +2,14 @@ package view
import (
"fmt"
+ "os"
+ "strconv"
+
"github.com/charmbracelet/bubbles/table"
tea "github.com/charmbracelet/bubbletea"
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/base/tablelist"
- "os"
- "strconv"
)
var columns = []table.Column{

View File

@ -14,6 +14,7 @@
package artifact
import (
"github.com/goharbor/harbor-cli/cmd/harbor/root/artifact/label"
"github.com/spf13/cobra"
)
@ -31,6 +32,7 @@ func Artifact() *cobra.Command {
DeleteArtifactCommand(),
ScanArtifactCommand(),
ArtifactTagsCmd(),
label.LabelsArtifactCommmand(),
)
return cmd

View File

@ -14,6 +14,8 @@
package artifact
import (
"fmt"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
@ -26,22 +28,27 @@ func DeleteArtifactCommand() *cobra.Command {
Use: "delete",
Short: "delete an artifact",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
var err error
var projectName, repoName, reference string
if len(args) > 0 {
projectName, repoName, reference := utils.ParseProjectRepoReference(args[0])
err = api.DeleteArtifact(projectName, repoName, reference)
projectName, repoName, reference, err = utils.ParseProjectRepoReference(args[0])
if err != nil {
return fmt.Errorf("failed to parse project/repo/reference: %v", err)
}
} else {
projectName := prompt.GetProjectNameFromUser()
repoName := prompt.GetRepoNameFromUser(projectName)
reference := prompt.GetReferenceFromUser(repoName, projectName)
err = api.DeleteArtifact(projectName, repoName, reference)
projectName, err = prompt.GetProjectNameFromUser()
if err != nil {
log.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
}
repoName = prompt.GetRepoNameFromUser(projectName)
reference = prompt.GetReferenceFromUser(repoName, projectName)
}
err = api.DeleteArtifact(projectName, repoName, reference)
if err != nil {
log.Errorf("failed to delete an artifact: %v", err)
return fmt.Errorf("failed to delete an artifact: %v", utils.ParseHarborErrorMsg(err))
}
return nil
},
}

View File

@ -0,0 +1,92 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package label
import (
"fmt"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/spf13/cobra"
)
// AddLabelArtifactCommmand adds a label to an artifact
func AddLabelArtifactCommmand() *cobra.Command {
cmd := &cobra.Command{
Use: "add",
Short: "Attach a label to an artifact in a Harbor project repository",
Long: `Attach an existing label to a specific artifact identified by <project>/<repository>:<reference>.
You can specify the artifact and label directly as arguments, or interactively select them if arguments are omitted.
Examples:
# Add a label to an artifact using project/repo:reference and label name
harbor artifact label add myproject/myrepo@sha256:abcdef1234567890 dev
# Prompt-based label selection for an artifact
harbor artifact label add library/nginx:1.21
# Fully interactive mode (prompt for everything)
harbor artifact label add
`,
Args: cobra.MaximumNArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
var (
projectName, repoName, reference string
labelName string
labelID int64
err error
)
if len(args) >= 1 {
projectName, repoName, reference, err = utils.ParseProjectRepoReference(args[0])
if err != nil {
return fmt.Errorf("failed to parse project/repo/reference: %v", err)
}
} else {
projectName, err = prompt.GetProjectNameFromUser()
if err != nil {
return fmt.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
}
repoName = prompt.GetRepoNameFromUser(projectName)
reference = prompt.GetReferenceFromUser(repoName, projectName)
}
if len(args) == 2 {
labelName = args[1]
labelID, err = api.GetLabelIdByName(labelName)
if err != nil {
return fmt.Errorf("failed to get label id: %v", utils.ParseHarborErrorMsg(err))
}
} else {
labels, err := api.ListLabel()
if err != nil {
return fmt.Errorf("failed to list labels: %v", utils.ParseHarborErrorMsg(err))
}
labelID = prompt.GetLabelIdFromUser(labels.Payload)
}
label := api.GetLabel(labelID)
if _, err := api.AddLabelArtifact(projectName, repoName, reference, label); err != nil {
return fmt.Errorf("failed to add label to artifact: %v", utils.ParseHarborErrorMsg(err))
}
fmt.Printf("Label '%s' added to artifact %s/%s@%s\n", label.Name, projectName, repoName, reference)
return nil
},
}
return cmd
}

View File

@ -0,0 +1,34 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package label
import (
"github.com/spf13/cobra"
)
// LabelsArtifactCommmand compound command to label artifacts
func LabelsArtifactCommmand() *cobra.Command {
cmd := &cobra.Command{
Use: "label",
Short: "label command for artifacts",
Long: `label command for artifact`,
Example: `harbor artifact label add <project>/<repository>/<reference> <label name>
harbor artifact label del <project>/<repository>/<reference> <label name>
`,
}
cmd.AddCommand(AddLabelArtifactCommmand())
cmd.AddCommand(DelLabelArtifactCommmand())
cmd.AddCommand(ListLabelArtifactCommmand())
return cmd
}

View File

@ -0,0 +1,97 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package label
import (
"fmt"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/spf13/cobra"
)
// DelLabelArtifactCommmand deletes a label from an artifact
func DelLabelArtifactCommmand() *cobra.Command {
cmd := &cobra.Command{
Use: "delete",
Aliases: []string{"del"},
Short: "Detach a label from an artifact in a Harbor project repository",
Long: `Remove an existing label from a specific artifact identified by <project>/<repository>:<reference>.
You can provide the artifact and label name as arguments, or choose them interactively if not specified.
Examples:
# Remove a label by specifying artifact and label name
harbor artifact label delete library/nginx:latest stable
# Prompt-based label selection for a specific artifact
harbor artifact label del library/nginx:1.21
# Fully interactive mode (prompt for project, repo, reference, and label)
harbor artifact label delete
# Remove a label from an artifact identified by digest
harbor artifact label del myproject/myrepo@sha256:abcdef1234567890 qa-label`,
RunE: func(cmd *cobra.Command, args []string) error {
var (
projectName, repoName, reference string
labelID int64
err error
)
if len(args) >= 1 {
projectName, repoName, reference, err = utils.ParseProjectRepoReference(args[0])
if err != nil {
return fmt.Errorf("failed to parse project/repo/reference: %v", err)
}
} else {
projectName, err = prompt.GetProjectNameFromUser()
if err != nil {
return fmt.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
}
repoName = prompt.GetRepoNameFromUser(projectName)
reference = prompt.GetReferenceFromUser(repoName, projectName)
}
if len(args) == 2 {
labelName := args[1]
labelID, err = api.GetLabelIdByName(labelName)
if err != nil {
return fmt.Errorf("failed to get label id: %v", utils.ParseHarborErrorMsg(err))
}
} else {
artifact, err := api.ViewArtifact(projectName, repoName, reference, true)
if err != nil || artifact == nil {
return fmt.Errorf("failed to get artifact info: %v", utils.ParseHarborErrorMsg(err))
}
labels := artifact.Payload.Labels
if len(labels) == 0 {
fmt.Printf("No labels found for artifact %s/%s@%s\n", projectName, repoName, reference)
return nil
}
labelID = prompt.GetLabelIdFromUser(labels)
}
if _, err := api.RemoveLabelArtifact(projectName, repoName, reference, labelID); err != nil {
return fmt.Errorf("failed to remove label from artifact: %v", utils.ParseHarborErrorMsg(err))
}
fmt.Println("Label removed successfully")
return nil
},
}
return cmd
}

View File

@ -0,0 +1,101 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package label
import (
"fmt"
"github.com/goharbor/go-client/pkg/sdk/v2.0/client/artifact"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/label/list"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// DelLabelArtifactCommmand delete label command to artifact
func ListLabelArtifactCommmand() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "Display labels attached to a specific artifact",
Long: `This command lists all labels currently associated with a specific artifact in a Harbor project repository.
You can provide the artifact reference in the format <project>/<repository>:<reference> (where reference is either a tag or a digest).
If the reference is not provided as an argument, the command will prompt you to select the project, repository, and artifact.
Supports output formatting such as JSON or YAML using the --output (-o) flag.`,
Example: ` # List labels for a tagged artifact
harbor artifact label list library/nginx:latest
# List labels for an artifact by digest
harbor artifact label list myproject/myrepo@sha256:abc123...
# Prompt-based interactive selection of artifact
harbor artifact label list
# Output in JSON format
harbor artifact label list library/nginx:1.21 -o json`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var err error
var projectName, repoName, reference string
var artifact *artifact.GetArtifactOK
getLabel := true
if len(args) > 0 {
projectName, repoName, reference, err = utils.ParseProjectRepoReference(args[0])
if err != nil {
return fmt.Errorf("failed to parse project/repo/reference: %v", err)
}
} else {
projectName, err = prompt.GetProjectNameFromUser()
if err != nil {
return fmt.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
}
repoName = prompt.GetRepoNameFromUser(projectName)
reference = prompt.GetReferenceFromUser(repoName, projectName)
}
if reference == "" {
if len(args) > 0 {
return fmt.Errorf("Invalid artifact reference format: %s", args[0])
} else {
return fmt.Errorf("Invalid artifact reference format: no arguments provided")
}
}
artifact, err = api.ViewArtifact(projectName, repoName, reference, getLabel)
if err != nil || artifact == nil {
return fmt.Errorf("failed to get info of an artifact: %v", utils.ParseHarborErrorMsg(err))
}
labelList := artifact.Payload.Labels
if len(labelList) == 0 {
fmt.Printf("No labels found for artifact %s/%s@%s", projectName, repoName, reference)
return nil
}
formatFlag := viper.GetString("output-format")
if formatFlag != "" {
err = utils.PrintFormat(labelList, formatFlag)
if err != nil {
return err
}
} else {
list.ListLabels(labelList)
}
return nil
},
}
return cmd
}

View File

@ -14,12 +14,13 @@
package artifact
import (
"fmt"
"github.com/goharbor/go-client/pkg/sdk/v2.0/client/artifact"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
artifactViews "github.com/goharbor/harbor-cli/pkg/views/artifact/list"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@ -29,36 +30,55 @@ func ListArtifactCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "list artifacts within a repository",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
Short: "List container artifacts (images, charts, etc.) in a Harbor repository with metadata",
Long: `List all artifacts (e.g., container images, charts) within a given Harbor repository.
Supports optional project/repository input in the form <project>/<repository>.
Displays key artifact metadata including tags, digest, type, size, vulnerability count, and push time.
Examples:
harbor-cli artifact list # Interactive prompt for project and repository
harbor-cli artifact list library/nginx # Directly list artifacts in the nginx repo under 'library' project
Supports pagination, search queries, and sorting using flags.`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if opts.PageSize > 100 {
return fmt.Errorf("page size should be less than or equal to 100")
}
var err error
var artifacts artifact.ListArtifactsOK
var projectName, repoName string
if len(args) > 0 {
projectName, repoName = utils.ParseProjectRepo(args[0])
projectName, repoName, err = utils.ParseProjectRepo(args[0])
if err != nil {
return fmt.Errorf("failed to parse project/repo: %v", err)
}
} else {
projectName = prompt.GetProjectNameFromUser()
projectName, err = prompt.GetProjectNameFromUser()
if err != nil {
return fmt.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
}
repoName = prompt.GetRepoNameFromUser(projectName)
}
artifacts, err = api.ListArtifact(projectName, repoName, opts)
if err != nil {
log.Errorf("failed to list artifacts: %v", err)
return
return fmt.Errorf("failed to list artifacts: %v", err)
}
FormatFlag := viper.GetString("output-format")
if FormatFlag != "" {
err = utils.PrintFormat(artifacts, FormatFlag)
if err != nil {
log.Error(err)
return err
}
} else {
artifactViews.ListArtifacts(artifacts.Payload)
}
return nil
},
}

View File

@ -14,6 +14,8 @@
package artifact
import (
"fmt"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
@ -44,21 +46,28 @@ func StartScanArtifactCommand() *cobra.Command {
Short: "Start a scan of an artifact",
Long: `Start a scan of an artifact in Harbor Repository`,
Example: `harbor artifact scan start <project>/<repository>/<reference>`,
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
var err error
var projectName, repoName, reference string
if len(args) > 0 {
projectName, repoName, reference := utils.ParseProjectRepoReference(args[0])
err = api.StartScanArtifact(projectName, repoName, reference)
projectName, repoName, reference, err = utils.ParseProjectRepoReference(args[0])
if err != nil {
return fmt.Errorf("failed to parse project/repo/reference: %v", err)
}
} else {
projectName := prompt.GetProjectNameFromUser()
repoName := prompt.GetRepoNameFromUser(projectName)
reference := prompt.GetReferenceFromUser(repoName, projectName)
err = api.StartScanArtifact(projectName, repoName, reference)
projectName, err = prompt.GetProjectNameFromUser()
if err != nil {
return fmt.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
}
repoName = prompt.GetRepoNameFromUser(projectName)
reference = prompt.GetReferenceFromUser(repoName, projectName)
}
err = api.StartScanArtifact(projectName, repoName, reference)
if err != nil {
log.Errorf("failed to start scan of artifact: %v", err)
return fmt.Errorf("failed to start scan of artifact: %v", err)
}
return nil
},
}
return cmd
@ -72,16 +81,24 @@ func StopScanArtifactCommand() *cobra.Command {
Example: `harbor artifact scan stop <project>/<repository>/<reference>`,
Run: func(cmd *cobra.Command, args []string) {
var err error
var projectName, repoName, reference string
if len(args) > 0 {
projectName, repoName, reference := utils.ParseProjectRepoReference(args[0])
err = api.StopScanArtifact(projectName, repoName, reference)
projectName, repoName, reference, err = utils.ParseProjectRepoReference(args[0])
if err != nil {
log.Errorf("failed to parse project/repo/reference: %v", err)
}
} else {
projectName := prompt.GetProjectNameFromUser()
repoName := prompt.GetRepoNameFromUser(projectName)
reference := prompt.GetReferenceFromUser(repoName, projectName)
err = api.StopScanArtifact(projectName, repoName, reference)
var projectName string
projectName, err = prompt.GetProjectNameFromUser()
if err != nil {
log.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
}
repoName = prompt.GetRepoNameFromUser(projectName)
reference = prompt.GetReferenceFromUser(repoName, projectName)
}
err = api.StopScanArtifact(projectName, repoName, reference)
if err != nil {
log.Errorf("failed to stop scan of artifact: %v", err)
}

View File

@ -48,19 +48,24 @@ func CreateTagsCmd() *cobra.Command {
Example: `harbor artifact tags create <project>/<repository>/<reference> <tag>`,
Run: func(cmd *cobra.Command, args []string) {
var err error
var projectName, repoName, reference string
var tagName string
if len(args) > 0 {
projectName, repoName, reference := utils.ParseProjectRepoReference(args[0])
tag := args[1]
err = api.CreateTag(projectName, repoName, reference, tag)
projectName, repoName, reference, err = utils.ParseProjectRepoReference(args[0])
if err != nil {
log.Errorf("failed to parse project/repo/reference: %v", err)
}
tagName = args[1]
} else {
var tagName string
projectName := prompt.GetProjectNameFromUser()
repoName := prompt.GetRepoNameFromUser(projectName)
reference := prompt.GetReferenceFromUser(repoName, projectName)
projectName, err = prompt.GetProjectNameFromUser()
if err != nil {
log.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
}
repoName = prompt.GetRepoNameFromUser(projectName)
reference = prompt.GetReferenceFromUser(repoName, projectName)
create.CreateTagView(&tagName)
err = api.CreateTag(projectName, repoName, reference, tagName)
}
err = api.CreateTag(projectName, repoName, reference, tagName)
if err != nil {
log.Errorf("failed to create tag: %v", err)
}
@ -81,10 +86,19 @@ func ListTagsCmd() *cobra.Command {
var projectName, repoName, reference string
if len(args) > 0 {
projectName, repoName, reference = utils.ParseProjectRepoReference(args[0])
projectName, repoName, reference, err = utils.ParseProjectRepoReference(args[0])
if err != nil {
log.Errorf("failed to parse project/repo/reference: %v", err)
}
} else {
projectName = prompt.GetProjectNameFromUser()
projectName, err = prompt.GetProjectNameFromUser()
if err != nil {
log.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
}
repoName = prompt.GetRepoNameFromUser(projectName)
if repoName == "" {
return
}
reference = prompt.GetReferenceFromUser(repoName, projectName)
}
@ -118,18 +132,24 @@ func DeleteTagsCmd() *cobra.Command {
Example: `harbor artifact tags delete <project>/<repository>/<reference> <tag>`,
Run: func(cmd *cobra.Command, args []string) {
var err error
var projectName, repoName, reference string
var tagName string
if len(args) > 0 {
projectName, repoName, reference := utils.ParseProjectRepoReference(args[0])
tag := args[1]
err = api.DeleteTag(projectName, repoName, reference, tag)
projectName, repoName, reference, err = utils.ParseProjectRepoReference(args[0])
if err != nil {
log.Errorf("failed to parse project/repo/reference: %v", err)
}
tagName = args[1]
} else {
projectName := prompt.GetProjectNameFromUser()
repoName := prompt.GetRepoNameFromUser(projectName)
reference := prompt.GetReferenceFromUser(repoName, projectName)
tag := prompt.GetTagFromUser(repoName, projectName, reference)
err = api.DeleteTag(projectName, repoName, reference, tag)
projectName, err = prompt.GetProjectNameFromUser()
if err != nil {
log.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
}
repoName = prompt.GetRepoNameFromUser(projectName)
reference = prompt.GetReferenceFromUser(repoName, projectName)
tagName = prompt.GetTagFromUser(repoName, projectName, reference)
}
err = api.DeleteTag(projectName, repoName, reference, tagName)
if err != nil {
log.Errorf("failed to delete tag: %v", err)
}

View File

@ -29,21 +29,36 @@ func ViewArtifactCommmand() *cobra.Command {
Use: "view",
Short: "Get information of an artifact",
Long: `Get information of an artifact`,
Example: `harbor artifact view <project>/<repository>/<reference>`,
Example: `harbor artifact view <project>/<repository>:<tag> OR harbor artifact view <project>/<repository>@<digest>`,
Run: func(cmd *cobra.Command, args []string) {
var err error
var projectName, repoName, reference string
var artifact *artifact.GetArtifactOK
if len(args) > 0 {
projectName, repoName, reference = utils.ParseProjectRepoReference(args[0])
projectName, repoName, reference, err = utils.ParseProjectRepoReference(args[0])
if err != nil {
log.Errorf("failed to parse project/repo/reference: %v", err)
}
} else {
projectName = prompt.GetProjectNameFromUser()
projectName, err = prompt.GetProjectNameFromUser()
if err != nil {
log.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
return
}
repoName = prompt.GetRepoNameFromUser(projectName)
reference = prompt.GetReferenceFromUser(repoName, projectName)
}
artifact, err = api.ViewArtifact(projectName, repoName, reference)
if reference == "" {
if len(args) > 0 {
log.Errorf("Invalid artifact reference format: %s", args[0])
} else {
log.Error("Invalid artifact reference format: no arguments provided")
}
}
artifact, err = api.ViewArtifact(projectName, repoName, reference, false)
if err != nil {
log.Errorf("failed to get info of an artifact: %v", err)

View File

@ -15,15 +15,28 @@ package root
import (
"fmt"
"io"
"time"
"github.com/goharbor/harbor-cli/cmd/harbor/root/artifact"
"github.com/goharbor/harbor-cli/cmd/harbor/root/context"
"github.com/goharbor/harbor-cli/cmd/harbor/root/cve"
"github.com/goharbor/harbor-cli/cmd/harbor/root/instance"
"github.com/goharbor/harbor-cli/cmd/harbor/root/labels"
"github.com/goharbor/harbor-cli/cmd/harbor/root/project"
"github.com/goharbor/harbor-cli/cmd/harbor/root/quota"
"github.com/goharbor/harbor-cli/cmd/harbor/root/registry"
repositry "github.com/goharbor/harbor-cli/cmd/harbor/root/repository"
"github.com/goharbor/harbor-cli/cmd/harbor/root/replication"
"github.com/goharbor/harbor-cli/cmd/harbor/root/repository"
"github.com/goharbor/harbor-cli/cmd/harbor/root/robot"
"github.com/goharbor/harbor-cli/cmd/harbor/root/scan_all"
"github.com/goharbor/harbor-cli/cmd/harbor/root/scanner"
"github.com/goharbor/harbor-cli/cmd/harbor/root/schedule"
"github.com/goharbor/harbor-cli/cmd/harbor/root/tag"
"github.com/goharbor/harbor-cli/cmd/harbor/root/user"
"github.com/goharbor/harbor-cli/cmd/harbor/root/webhook"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@ -53,6 +66,18 @@ harbor help
// Initialize configuration
utils.InitConfig(cfgFile, userSpecifiedConfig)
// Conditionally set the timestamp format only in verbose mode
formatter := &logrus.TextFormatter{}
if verbose {
formatter.FullTimestamp = true
formatter.TimestampFormat = time.RFC3339
logrus.SetLevel(logrus.DebugLevel)
} else {
logrus.SetOutput(io.Discard)
}
logrus.SetFormatter(formatter)
return nil
},
}
@ -71,18 +96,102 @@ harbor help
fmt.Println(err.Error())
}
root.AddCommand(
versionCommand(),
LoginCommand(),
project.Project(),
registry.Registry(),
repositry.Repository(),
user.User(),
artifact.Artifact(),
HealthCommand(),
schedule.Schedule(),
labels.Labels(),
)
root.AddGroup(&cobra.Group{ID: "core", Title: "Core:"})
root.AddGroup(&cobra.Group{ID: "access", Title: "Access:"})
root.AddGroup(&cobra.Group{ID: "system", Title: "System:"})
root.AddGroup(&cobra.Group{ID: "utils", Title: "Utility:"})
// Core
cmd := InfoCommand()
cmd.GroupID = "core"
root.AddCommand(cmd)
cmd = project.Project()
cmd.GroupID = "core"
root.AddCommand(cmd)
cmd = repository.Repository()
cmd.GroupID = "core"
root.AddCommand(cmd)
cmd = artifact.Artifact()
cmd.GroupID = "core"
root.AddCommand(cmd)
cmd = tag.TagCommand()
cmd.GroupID = "core"
root.AddCommand(cmd)
cmd = labels.Labels()
cmd.GroupID = "core"
root.AddCommand(cmd)
cmd = quota.Quota()
cmd.GroupID = "core"
root.AddCommand(cmd)
cmd = cve.CVEAllowlist()
cmd.GroupID = "core"
root.AddCommand(cmd)
cmd = webhook.Webhook()
cmd.GroupID = "core"
root.AddCommand(cmd)
cmd = robot.Robot()
cmd.GroupID = "core"
root.AddCommand(cmd)
// Access
cmd = LoginCommand()
cmd.GroupID = "access"
root.AddCommand(cmd)
cmd = user.User()
cmd.GroupID = "access"
root.AddCommand(cmd)
// System
cmd = context.Context()
cmd.GroupID = "system"
root.AddCommand(cmd)
cmd = HealthCommand()
cmd.GroupID = "system"
root.AddCommand(cmd)
cmd = instance.Instance()
cmd.GroupID = "system"
root.AddCommand(cmd)
cmd = registry.Registry()
cmd.GroupID = "system"
root.AddCommand(cmd)
cmd = replication.Replication()
cmd.GroupID = "system"
root.AddCommand(cmd)
cmd = scanner.Scanner()
cmd.GroupID = "system"
root.AddCommand(cmd)
cmd = scan_all.ScanAll()
cmd.GroupID = "system"
root.AddCommand(cmd)
cmd = schedule.Schedule()
cmd.GroupID = "system"
root.AddCommand(cmd)
// Utils
cmd = versionCommand()
cmd.GroupID = "utils"
root.AddCommand(cmd)
cmd = Logs()
cmd.GroupID = "utils"
root.AddCommand(cmd)
return root
}

View File

@ -0,0 +1,36 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package context
import "github.com/spf13/cobra"
func Context() *cobra.Command {
cmd := &cobra.Command{
Use: "context",
Short: "Manage locally available contexts",
Example: "harbor context list",
Long: `The context command allows you to manage configuration items of the Harbor CLI.
You can add, get, or delete specific configuration items, as well as list all configuration items of the Harbor CLI.`,
}
cmd.AddCommand(
ListContextCommand(),
GetContextItemCommand(),
UpdateContextItemCommand(),
DeleteContextItemCommand(),
SwitchContextCommand(),
)
return cmd
}

View File

@ -0,0 +1,417 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package context_test
import (
"testing"
"github.com/goharbor/harbor-cli/cmd/harbor/root"
"github.com/goharbor/harbor-cli/pkg/utils"
helpers "github.com/goharbor/harbor-cli/test/helper"
"github.com/stretchr/testify/assert"
)
func Test_ContextCmd(t *testing.T) {
tempDir := t.TempDir()
data := helpers.Initialize(t, tempDir)
defer helpers.ConfigCleanup(t, data)
helpers.SetMockKeyring(t)
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"context"})
err := rootCmd.Execute()
assert.Nil(t, err)
}
func Test_ContextListCmd(t *testing.T) {
tempDir := t.TempDir()
data := helpers.Initialize(t, tempDir)
defer helpers.ConfigCleanup(t, data)
helpers.SetMockKeyring(t)
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"context", "list"})
err := rootCmd.Execute()
assert.Nil(t, err)
}
func Test_ContextGetCmd_Success(t *testing.T) {
tempDir := t.TempDir()
data := helpers.Initialize(t, tempDir)
defer helpers.ConfigCleanup(t, data)
helpers.SetMockKeyring(t)
testConfig := &utils.HarborConfig{
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
Credentials: []utils.Credential{
{
Name: "harbor-cli@http://demo.goharbor.io",
ServerAddress: "http://demo.goharbor.io",
Username: "harbor-cli",
Password: "Harbor12345",
},
},
}
err := utils.UpdateConfigFile(testConfig)
if err != nil {
t.Fatal(err)
}
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"context", "get", "credentials.serveraddress"})
err = rootCmd.Execute()
assert.NoError(t, err)
}
func Test_ContextGetCmd_Failure(t *testing.T) {
tempDir := t.TempDir()
data := helpers.Initialize(t, tempDir)
defer helpers.ConfigCleanup(t, data)
helpers.SetMockKeyring(t)
testConfig := &utils.HarborConfig{
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
Credentials: []utils.Credential{
{
Name: "harbor-cli@http://demo.goharbor.io",
ServerAddress: "http://demo.goharbor.io",
Username: "harbor-cli",
Password: "Harbor12345",
},
},
}
err := utils.UpdateConfigFile(testConfig)
if err != nil {
t.Fatal(err)
}
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"context", "get", "serveraddress"})
err = rootCmd.Execute()
assert.Error(t, err, "Expected an error when getting a non-existent config item")
}
func Test_ContextGetCmd_CredentialName_Success(t *testing.T) {
tempDir := t.TempDir()
data := helpers.Initialize(t, tempDir)
defer helpers.ConfigCleanup(t, data)
helpers.SetMockKeyring(t)
testConfig := &utils.HarborConfig{
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
Credentials: []utils.Credential{
{
Name: "harbor-cli@http://demo.goharbor.io",
ServerAddress: "http://demo.goharbor.io",
Username: "harbor-cli",
Password: "Harbor12345",
},
},
}
err := utils.UpdateConfigFile(testConfig)
if err != nil {
t.Fatal(err)
}
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"context", "get", "credentials.serveraddress", "--name", "harbor-cli@http://demo.goharbor.io"})
err = rootCmd.Execute()
assert.NoError(t, err)
}
func Test_ContextGetCmd_CredentialName_Failure(t *testing.T) {
tempDir := t.TempDir()
data := helpers.Initialize(t, tempDir)
defer helpers.ConfigCleanup(t, data)
helpers.SetMockKeyring(t)
testConfig := &utils.HarborConfig{
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
Credentials: []utils.Credential{
{
Name: "harbor-cli@http://demo.goharbor.io",
ServerAddress: "http://demo.goharbor.io",
Username: "harbor-cli",
Password: "Harbor12345",
},
},
}
err := utils.UpdateConfigFile(testConfig)
if err != nil {
t.Fatal(err)
}
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"context", "get", "credentials.serveraddress", "--name", "harbor-cli@http://goharbor.io"})
err = rootCmd.Execute()
assert.Error(t, err, "Expected an error when getting a non-existent credential name")
}
func Test_ContextUpdateCmd_Success(t *testing.T) {
tempDir := t.TempDir()
data := helpers.Initialize(t, tempDir)
defer helpers.ConfigCleanup(t, data)
helpers.SetMockKeyring(t)
testConfig := &utils.HarborConfig{
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
Credentials: []utils.Credential{
{
Name: "harbor-cli@http://demo.goharbor.io",
ServerAddress: "http://demo.goharbor.io",
Username: "harbor-cli",
Password: "Harbor12345",
},
},
}
err := utils.UpdateConfigFile(testConfig)
if err != nil {
t.Fatal(err)
}
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"context", "update", "credentials.serveraddress", "http://demo.goharbor.io"})
err = rootCmd.Execute()
assert.NoError(t, err)
}
func Test_ContextUpdateCmd_CredentialName_Success(t *testing.T) {
tempDir := t.TempDir()
data := helpers.Initialize(t, tempDir)
defer helpers.ConfigCleanup(t, data)
helpers.SetMockKeyring(t)
testConfig := &utils.HarborConfig{
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
Credentials: []utils.Credential{
{
Name: "harbor-cli@http://demo.goharbor.io",
ServerAddress: "http://demo.goharbor.io",
Username: "harbor-cli",
Password: "Harbor12345",
},
},
}
err := utils.UpdateConfigFile(testConfig)
if err != nil {
t.Fatal(err)
}
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"context", "update", "credentials.serveraddress", "http://demo.goharbor.io", "--name", "harbor-cli@http://demo.goharbor.io"})
err = rootCmd.Execute()
assert.NoError(t, err)
}
func Test_ContextUpdateCmd_CredentialName_Failure(t *testing.T) {
tempDir := t.TempDir()
data := helpers.Initialize(t, tempDir)
defer helpers.ConfigCleanup(t, data)
helpers.SetMockKeyring(t)
testConfig := &utils.HarborConfig{
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
Credentials: []utils.Credential{
{
Name: "harbor-cli@http://demo.goharbor.io",
ServerAddress: "http://demo.goharbor.io",
Username: "harbor-cli",
Password: "Harbor12345",
},
},
}
err := utils.UpdateConfigFile(testConfig)
if err != nil {
t.Fatal(err)
}
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"context", "update", "credentials.serveraddress", "http://demo.goharbor.io", "--name", "harbor-cli@http://goharbor.io"})
err = rootCmd.Execute()
assert.Error(t, err, "Expected an error when setting a non-existent credential name")
}
func Test_ContextUpdateCmd_Failure(t *testing.T) {
tempDir := t.TempDir()
data := helpers.Initialize(t, tempDir)
defer helpers.ConfigCleanup(t, data)
helpers.SetMockKeyring(t)
testConfig := &utils.HarborConfig{
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
Credentials: []utils.Credential{
{
Name: "harbor-cli@http://demo.goharbor.io",
ServerAddress: "http://demo.goharbor.io",
Username: "harbor-cli",
Password: "Harbor12345",
},
},
}
err := utils.UpdateConfigFile(testConfig)
if err != nil {
t.Fatal(err)
}
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"context", "update", "serveraddress", "http://demo.goharbor.io"})
err = rootCmd.Execute()
assert.Error(t, err, "Expected an error when setting a non-existent config item")
}
func Test_ContextDeleteCmd_Success(t *testing.T) {
tempDir := t.TempDir()
data := helpers.Initialize(t, tempDir)
defer helpers.ConfigCleanup(t, data)
helpers.SetMockKeyring(t)
testConfig := &utils.HarborConfig{
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
Credentials: []utils.Credential{
{
Name: "harbor-cli@http://demo.goharbor.io",
ServerAddress: "http://demo.goharbor.io",
Username: "harbor-cli",
Password: "Harbor12345",
},
},
}
err := utils.UpdateConfigFile(testConfig)
if err != nil {
t.Fatal(err)
}
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"context", "delete", "credentials.serveraddress"})
err = rootCmd.Execute()
assert.NoError(t, err)
config, err := utils.GetCurrentHarborConfig()
if err != nil {
t.Fatal(err)
}
assert.Empty(t, config.Credentials[0].ServerAddress)
}
func Test_ContextDeleteCmd_Failure(t *testing.T) {
tempDir := t.TempDir()
data := helpers.Initialize(t, tempDir)
defer helpers.ConfigCleanup(t, data)
helpers.SetMockKeyring(t)
testConfig := &utils.HarborConfig{
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
Credentials: []utils.Credential{
{
Name: "harbor-cli@http://demo.goharbor.io",
ServerAddress: "http://demo.goharbor.io",
Username: "harbor-cli",
Password: "Harbor12345",
},
},
}
err := utils.UpdateConfigFile(testConfig)
if err != nil {
t.Fatal(err)
}
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"context", "delete", "serveraddress"})
err = rootCmd.Execute()
assert.Error(t, err, "Expected an error when deleting a non-existent config item")
}
func Test_ContextDeleteCmd_CredentialName_Success(t *testing.T) {
tempDir := t.TempDir()
data := helpers.Initialize(t, tempDir)
defer helpers.ConfigCleanup(t, data)
helpers.SetMockKeyring(t)
testConfig := &utils.HarborConfig{
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
Credentials: []utils.Credential{
{
Name: "harbor-cli@http://demo.goharbor.io",
ServerAddress: "http://demo.goharbor.io",
Username: "harbor-cli",
Password: "Harbor12345",
},
},
}
err := utils.UpdateConfigFile(testConfig)
if err != nil {
t.Fatal(err)
}
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"context", "delete", "credentials.serveraddress", "--name", "harbor-cli@http://demo.goharbor.io"})
err = rootCmd.Execute()
assert.NoError(t, err)
config, err := utils.GetCurrentHarborConfig()
if err != nil {
t.Fatal(err)
}
assert.Empty(t, config.Credentials[0].ServerAddress)
}
func Test_ContextDeleteCmd_CredentialName_Failure(t *testing.T) {
tempDir := t.TempDir()
data := helpers.Initialize(t, tempDir)
defer helpers.ConfigCleanup(t, data)
helpers.SetMockKeyring(t)
testConfig := &utils.HarborConfig{
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
Credentials: []utils.Credential{
{
Name: "harbor-cli@http://demo.goharbor.io",
ServerAddress: "http://demo.goharbor.io",
Username: "harbor-cli",
Password: "Harbor12345",
},
},
}
err := utils.UpdateConfigFile(testConfig)
if err != nil {
t.Fatal(err)
}
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"context", "delete", "credentials.serveraddress", "--name", "harbor-cli@http://goharbor.io"})
err = rootCmd.Execute()
assert.Error(t, err, "Expected an error when deleting a non-existent credential name")
}
func Test_ContextDeleteCmd_Current_Flag_Success(t *testing.T) {
tempDir := t.TempDir()
data := helpers.Initialize(t, tempDir)
defer helpers.ConfigCleanup(t, data)
helpers.SetMockKeyring(t)
testConfig := &utils.HarborConfig{
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
Credentials: []utils.Credential{
{
Name: "harbor-cli@http://demo.goharbor.io",
ServerAddress: "http://demo.goharbor.io",
Username: "harbor-cli",
Password: "Harbor12345",
},
{
Name: "admin@http://demo.goharbor.io",
ServerAddress: "http://demo.goharbor.io",
Username: "admin",
Password: "Admin12345",
},
},
}
err := utils.UpdateConfigFile(testConfig)
if err != nil {
t.Fatal(err)
}
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"context", "delete", "--current"})
err = rootCmd.Execute()
assert.NoError(t, err)
config, err := utils.GetCurrentHarborConfig()
if err != nil {
t.Fatal(err)
}
assert.Empty(t, config.CurrentCredentialName)
assert.NotEmpty(t, config.Credentials)
assert.NoError(t, err)
}
func Test_ContextDeleteCmd_Current_Flag_With_Item_Failure(t *testing.T) {
tempDir := t.TempDir()
data := helpers.Initialize(t, tempDir)
defer helpers.ConfigCleanup(t, data)
helpers.SetMockKeyring(t)
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"context", "delete", "credentials.serveraddress", "--current"})
err := rootCmd.Execute()
assert.Error(t, err, "Expected an error when specifying both --current and an item")
}

View File

@ -0,0 +1,244 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package context
import (
"fmt"
"reflect"
"strings"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var deleteCurrent bool
// DeleteContextItemCommand creates the 'harbor context delete' subcommand,
// allowing you to do: harbor context delete <item>
func DeleteContextItemCommand() *cobra.Command {
var credentialName string
cmd := &cobra.Command{
Use: "delete <item>",
Short: "Delete (clear) a specific config item",
Example: `
# Clear the current credential's password
harbor context delete credentials.password
# Clear a specific credential's password using --name
harbor context delete credentials.password --name admin@http://demo.goharbor.io
# Clear the current credential
harbor context delete --current
`,
Long: `Clear the value of a specific CLI config item by setting it to its zero value.
Case-insensitive field lookup, but uses the canonical (Go) field name internally.
If you specify --name, that credential (rather than the "current" one) will be used.`,
Args: cobra.RangeArgs(0, 1),
// Use RunE so we can propagate errors
RunE: func(cmd *cobra.Command, args []string) error {
// 1. Load the current config
config, err := utils.GetCurrentHarborConfig()
if err != nil {
return fmt.Errorf("failed to load Harbor config: %w", err)
}
// 1a. If --current is set, remove only the credential matching CurrentCredentialName
if deleteCurrent {
if len(args) > 0 {
return fmt.Errorf("cannot specify both <item> and --current")
}
currentName := config.CurrentCredentialName
found := false
if currentName != "" {
for i, cred := range config.Credentials {
if strings.EqualFold(cred.Name, currentName) {
// Remove just this credential
config.Credentials = append(config.Credentials[:i], config.Credentials[i+1:]...)
found = true
break
}
}
config.CurrentCredentialName = ""
}
if err := utils.UpdateConfigFile(config); err != nil {
return fmt.Errorf("failed to save updated config: %w", err)
}
if found {
logrus.Infof("Removed credential '%s' and cleared CurrentCredentialName", currentName)
} else {
logrus.Infof("No credential named '%s' found; cleared CurrentCredentialName anyway", currentName)
}
return nil
}
// If --all is NOT set, we'll perform the normal item-based delete.
// Check we actually received an item (since now it's optional).
if len(args) == 0 {
return fmt.Errorf("please specify an <item> or use --current")
}
// 2. Parse the user-supplied item path (e.g., "credentials.password")
itemPath := strings.Split(args[0], ".")
// 3. Reflection-based delete (zero out)
actualSegments := []string{}
if err := deleteValueInConfig(config, itemPath, &actualSegments, credentialName); err != nil {
return fmt.Errorf("failed to delete value in config: %w", err)
}
// 4. Persist the updated config to disk
if err := utils.UpdateConfigFile(config); err != nil {
return fmt.Errorf("failed to save updated config: %w", err)
}
// 5. Confirm to the user (no error here)
canonicalPath := strings.Join(actualSegments, ".")
logrus.Infof("Successfully cleared %s", canonicalPath)
return nil
},
}
// Add --name / -n to let the user pick a specific credential
cmd.Flags().StringVarP(
&credentialName,
"name",
"n",
"",
"Name of the credential to delete fields from (default: the current credential)",
)
cmd.Flags().BoolVar(
&deleteCurrent,
"current",
false,
"Remove current credentials from the config",
)
return cmd
}
// deleteValueInConfig checks whether the user is deleting something
// under "credentials" (i.e., *a* credential) or a top-level field.
//
// If the user says "credentials.*" AND provides --name, we'll look
// up that specific credential by name. Otherwise, we use CurrentCredentialName.
func deleteValueInConfig(
config *utils.HarborConfig,
path []string,
actualSegments *[]string,
credentialName string,
) error {
if len(path) == 0 {
return fmt.Errorf("no config item specified")
}
// If the first segment is "credentials", pivot to the chosen credential.
if strings.EqualFold(path[0], "credentials") {
*actualSegments = append(*actualSegments, "Credentials")
// Figure out which credential name to use
credName := config.CurrentCredentialName
if credentialName != "" {
credName = credentialName
}
// Find the matching credential
var targetCred *utils.Credential
for i := range config.Credentials {
if strings.EqualFold(config.Credentials[i].Name, credName) {
targetCred = &config.Credentials[i]
break
}
}
if targetCred == nil {
return fmt.Errorf("no matching credential found for '%s'", credName)
}
// Remove "credentials" from path, delete the value in that credential
return deleteNestedValue(targetCred, path[1:], actualSegments)
}
// Otherwise, we delete a field in the main HarborConfig struct
return deleteNestedValue(config, path, actualSegments)
}
// deleteNestedValue navigates a pointer to a struct, following the path segments
// in a case-insensitive manner, until the last segment, where it sets the field
// to its zero value.
func deleteNestedValue(obj interface{}, path []string, actualSegments *[]string) error {
// We require obj to be a pointer to a struct so we can modify it.
val := reflect.ValueOf(obj)
if val.Kind() != reflect.Ptr {
return fmt.Errorf("object must be a pointer to a struct, got %s", val.Kind())
}
val = val.Elem() // dereference pointer
for i, segment := range path {
if val.Kind() != reflect.Struct {
return fmt.Errorf("cannot traverse non-struct for segment '%s'", segment)
}
t := val.Type()
// Case-insensitive field lookup
fieldIndex := -1
for j := 0; j < val.NumField(); j++ {
if strings.EqualFold(t.Field(j).Name, segment) {
fieldIndex = j
break
}
}
if fieldIndex < 0 {
return fmt.Errorf("config item '%s' does not exist", segment)
}
field := t.Field(fieldIndex)
fieldValue := val.Field(fieldIndex)
// Record the actual field name
*actualSegments = append(*actualSegments, field.Name)
// If this is NOT the last path segment, move deeper
if i < len(path)-1 {
// If the field is a pointer and nil, we can't go deeper
if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() {
return fmt.Errorf("field '%s' is nil and cannot be traversed", field.Name)
}
// Descend
val = fieldValue
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
continue
}
// If this is the last segment, set the field to zero value
if !fieldValue.CanSet() {
return fmt.Errorf("cannot set field '%s' to zero value", field.Name)
}
// The zero value for that field can be obtained with reflect.Zero().
zeroVal := reflect.Zero(fieldValue.Type())
fieldValue.Set(zeroVal)
}
return nil
}

View File

@ -0,0 +1,196 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package context
import (
"encoding/json"
"fmt"
"reflect"
"strings"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"gopkg.in/yaml.v3"
)
// GetConfigItemCommand creates the 'harbor config get' subcommand.
func GetContextItemCommand() *cobra.Command {
var credentialName string
cmd := &cobra.Command{
Use: "get <item>",
Short: "Get a specific config item",
Example: `
# Get the current credential's username
harbor context get credentials.username
# Get a credential's username by specifying the credential name
harbor config get credentials.username --name admin@http://demo.goharbor.io
`,
Long: `Get the value of a specific CLI config item.
If you specify --name, that credential (rather than the "current" one) will be used.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
// 1. Load config
config, err := utils.GetCurrentHarborConfig()
if err != nil {
return fmt.Errorf("failed to get config: %w", err)
}
// 2. Parse the user-supplied item path (e.g., "credentials.username")
itemPath := strings.Split(args[0], ".")
// 3. Get the value from the config (and track actual field segments for output)
actualSegments := []string{}
result, err := getValueFromConfig(config, itemPath, &actualSegments, credentialName)
if err != nil {
return err
}
// 4. Prepare the final output as a map for JSON/YAML rendering.
canonicalPath := strings.Join(actualSegments, ".")
output := map[string]interface{}{
canonicalPath: result,
}
// 5. Determine the output format (json, yaml, etc.) and print.
formatFlag := viper.GetString("output-format")
switch formatFlag {
case "json":
data, err := json.MarshalIndent(output, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal output to JSON: %w", err)
}
fmt.Println(string(data))
case "yaml", "":
data, err := yaml.Marshal(output)
if err != nil {
return fmt.Errorf("failed to marshal output to YAML: %w", err)
}
fmt.Println(string(data))
default:
return fmt.Errorf("unsupported output format: %s", formatFlag)
}
return nil
},
}
// Add a --name / -n flag to allow specifying a credential
cmd.Flags().StringVarP(
&credentialName,
"name",
"n",
"",
"Name of the credential to get fields from (default: the current credential)",
)
return cmd
}
// getValueFromConfig decides if the user requested something under "credentials"
// and if so, filters down to the *requested credential*, otherwise
// it just searches in the top-level config object.
func getValueFromConfig(
config *utils.HarborConfig,
path []string,
actualSegments *[]string,
credentialName string,
) (interface{}, error) {
if len(path) == 0 {
return nil, fmt.Errorf("no config item specified")
}
// If the first segment is "credentials", we pivot to a credential.
if strings.EqualFold(path[0], "credentials") {
*actualSegments = append(*actualSegments, "Credentials")
// Determine which credential name to use
credName := config.CurrentCredentialName
if credentialName != "" {
credName = credentialName
}
// Find the matching credential
var targetCred *utils.Credential
for i := range config.Credentials {
if strings.EqualFold(config.Credentials[i].Name, credName) {
targetCred = &config.Credentials[i]
break
}
}
if targetCred == nil {
return nil, fmt.Errorf("no matching credential found for '%s'", credName)
}
// Remove "credentials" from the path, keep the rest
return getNestedValue(*targetCred, path[1:], actualSegments)
}
// Otherwise, search in the overall config struct
return getNestedValue(*config, path, actualSegments)
}
// getNestedValue uses reflection to walk through struct fields
// (case-insensitive) according to the provided path.
//
// 'actualSegments' is updated with the actual field names as we go.
func getNestedValue(obj interface{}, path []string, actualSegments *[]string) (interface{}, error) {
current := reflect.ValueOf(obj)
for _, key := range path {
// If it's a pointer, dereference
if current.Kind() == reflect.Ptr {
current = current.Elem()
}
if current.Kind() != reflect.Struct {
return nil, fmt.Errorf("cannot traverse non-struct for key '%s'", key)
}
// Find the actual field by name, ignoring case
var foundField reflect.StructField
var fieldValue reflect.Value
fieldFound := false
t := current.Type()
for i := 0; i < current.NumField(); i++ {
field := t.Field(i)
if strings.EqualFold(field.Name, key) {
foundField = field
fieldValue = current.Field(i)
fieldFound = true
break
}
}
if !fieldFound {
return nil, fmt.Errorf("config item '%s' does not exist", key)
}
// Record the *actual* field name in our slice
*actualSegments = append(*actualSegments, foundField.Name)
// Descend for the next iteration
current = fieldValue
}
// Finally, if we ended on a pointer, dereference it
if current.Kind() == reflect.Ptr {
current = current.Elem()
}
return current.Interface(), nil
}

View File

@ -0,0 +1,61 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package context
import (
"fmt"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/context/list"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func ListContextCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List contexts",
Example: ` harbor context list`,
Args: cobra.MaximumNArgs(0),
Run: func(cmd *cobra.Command, args []string) {
config, err := utils.GetCurrentHarborConfig()
if err != nil {
fmt.Println("failed to get config: ", utils.ParseHarborErrorMsg(err))
return
}
// Get the output format
formatFlag := viper.GetString("output-format")
if formatFlag != "" {
// Use utils.PrintFormat if available
err = utils.PrintFormat(config, formatFlag)
if err != nil {
fmt.Println("Failed to print config: ", utils.ParseHarborErrorMsg(err))
return
}
} else {
var cxlist []api.ContextListView
for _, cred := range config.Credentials {
cx := api.ContextListView{Name: cred.Name, Username: cred.Username, Server: cred.ServerAddress}
cxlist = append(cxlist, cx)
}
currentCredential := config.CurrentCredentialName
list.ListContexts(cxlist, currentCredential)
}
},
}
return cmd
}

View File

@ -0,0 +1,75 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package context
import (
"fmt"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/spf13/cobra"
)
func SwitchContextCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "switch <none|context>",
Short: "Switch to a new context",
Example: `harbor context switch harbor-cli@https-demo-goharbor-io`,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
config, err := utils.GetCurrentHarborConfig()
if err != nil {
fmt.Println("failed to get config: ", utils.ParseHarborErrorMsg(err))
return
}
if len(args) == 1 {
newActiveCredential := args[0]
found := false
for _, cred := range config.Credentials {
if cred.Name == newActiveCredential {
found = true
break
}
}
if found {
config.CurrentCredentialName = newActiveCredential
if err := utils.UpdateConfigFile(config); err != nil {
fmt.Println("failed to update config: ", utils.ParseHarborErrorMsg(err))
}
} else {
fmt.Println("context doesn't exist")
}
} else {
res, err := prompt.GetActiveContextFromUser()
if err != nil {
fmt.Println("failed to get active context: ", utils.ParseHarborErrorMsg(err))
return
}
if res != "" {
msg := fmt.Sprintf("context switched from '%s' to '%s'", config.CurrentCredentialName, res)
config.CurrentCredentialName = res
if err := utils.UpdateConfigFile(config); err != nil {
fmt.Println("failed to update config: ", utils.ParseHarborErrorMsg(err))
} else {
fmt.Println(msg)
}
}
}
},
}
return cmd
}

View File

@ -0,0 +1,267 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package context
import (
"fmt"
"reflect"
"strconv"
"strings"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// UpdateConfigItemCommand creates the 'harbor config update' subcommand,
// allowing you to do: harbor config update <item> <value>.
func UpdateContextItemCommand() *cobra.Command {
var credentialName string
cmd := &cobra.Command{
Use: "update <item> <value>",
Short: "Set/update a specific config item",
Example: `
# Set/update the current credential's password
harbor context update credentials.password myNewSecret
# Set/update a credential's password by specifying the credential name
harbor config update credentials.password myNewSecret --name admin@http://demo.goharbor.io
`,
Long: `Set/update the value of a specific CLI config item.
Case-insensitive field lookup, but uses the canonical (Go) field name internally.
If you specify --name, that credential (rather than the "current" one) will be updated.`,
Args: cobra.ExactArgs(2),
// Switch from Run to RunE so we can propagate errors
RunE: func(cmd *cobra.Command, args []string) error {
// 1. Load the current config
config, err := utils.GetCurrentHarborConfig()
if err != nil {
return fmt.Errorf("failed to load Harbor config: %w", err)
}
// 2. Parse the user-supplied item path (e.g., "credentials.password")
itemPath := strings.Split(args[0], ".")
newValue := args[1]
// 3. Reflection-based set
actualSegments := []string{}
if err := setValueInConfig(config, itemPath, newValue, &actualSegments, credentialName); err != nil {
return fmt.Errorf("failed to set value in config: %w", err)
}
// 4. Persist the updated config to disk
if err := utils.UpdateConfigFile(config); err != nil {
return fmt.Errorf("failed to save updated config: %w", err)
}
// 5. Confirm to the user (logrus.Info is fine here; no error)
canonicalPath := strings.Join(actualSegments, ".")
logrus.Infof("Successfully updated %s to '%s'", canonicalPath, newValue)
return nil
},
}
// Add a --name / -n flag to allow specifying a credential
cmd.Flags().StringVarP(
&credentialName,
"name",
"n",
"",
"Name of the credential to set fields on (default: the current credential)",
)
return cmd
}
// setValueInConfig checks whether the user is updating something
// under "credentials" (i.e., a credential) or a top-level field.
//
// If path[0] == "credentials", we decide which credential to modify:
// - If credentialName is non-empty, use that
// - Otherwise, fallback to config.CurrentCredentialName
func setValueInConfig(
config *utils.HarborConfig,
path []string,
newValue string,
actualSegments *[]string,
credentialName string,
) error {
if len(path) == 0 {
return fmt.Errorf("no config item specified")
}
// If the first segment is "credentials", then we pivot to a specific credential.
if strings.EqualFold(path[0], "credentials") {
*actualSegments = append(*actualSegments, "Credentials")
// Determine which credential name to use
credName := config.CurrentCredentialName
if credentialName != "" {
credName = credentialName
}
// find the matching credential
var matchingCred *utils.Credential
for i := range config.Credentials {
if strings.EqualFold(config.Credentials[i].Name, credName) {
matchingCred = &config.Credentials[i]
break
}
}
if matchingCred == nil {
return fmt.Errorf("no matching credential found for '%s'", credName)
}
// Remove "credentials" from the path, and set the value in that credential
return setNestedValue(matchingCred, path[1:], newValue, actualSegments)
}
// Otherwise, we set a field in the main HarborConfig struct
return setNestedValue(config, path, newValue, actualSegments)
}
// setNestedValue navigates a pointer to a struct, following the path segments
// in a case-insensitive manner, until the last segment, where it sets the value.
//
// If the last segment is Credentials.Password, it encrypts the user-supplied
// password before storing it.
func setNestedValue(obj interface{}, path []string, newValue string, actualSegments *[]string) error {
// We require obj to be a pointer to a struct so we can modify it.
val := reflect.ValueOf(obj)
if val.Kind() != reflect.Ptr {
return fmt.Errorf("object must be a pointer to a struct, got %s", val.Kind())
}
val = val.Elem() // dereference pointer
for i, segment := range path {
if val.Kind() != reflect.Struct {
return fmt.Errorf("cannot traverse non-struct for segment '%s'", segment)
}
t := val.Type()
// Case-insensitive field lookup
fieldIndex := -1
for j := 0; j < val.NumField(); j++ {
if strings.EqualFold(t.Field(j).Name, segment) {
fieldIndex = j
break
}
}
if fieldIndex < 0 {
return fmt.Errorf("config item '%s' does not exist", segment)
}
field := t.Field(fieldIndex)
fieldValue := val.Field(fieldIndex)
// Record the actual field name
*actualSegments = append(*actualSegments, field.Name)
// If this is NOT the last path segment, move deeper
if i < len(path)-1 {
// If the field is a pointer and nil, allocate a new instance
if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() {
newElem := reflect.New(fieldValue.Type().Elem())
fieldValue.Set(newElem)
}
// Descend
val = fieldValue
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
continue
}
// If this is the last segment, set the value
if !fieldValue.CanSet() {
return fmt.Errorf("cannot set field '%s'", field.Name)
}
switch fieldValue.Kind() {
case reflect.String:
// Special case: If we are setting Credentials.Password, encrypt it
// We'll check the last two actual segments, e.g. ["Credentials", "Password"].
if isCredentialsPassword(*actualSegments) {
encrypted, err := encryptPassword(newValue)
if err != nil {
return err
}
fieldValue.SetString(encrypted)
} else {
fieldValue.SetString(newValue)
}
case reflect.Bool:
boolVal, err := strconv.ParseBool(newValue)
if err != nil {
return fmt.Errorf("field '%s' expects a bool, but got '%s'", field.Name, newValue)
}
fieldValue.SetBool(boolVal)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
intVal, err := strconv.ParseInt(newValue, 10, 64)
if err != nil {
return fmt.Errorf("field '%s' expects an integer, but got '%s'", field.Name, newValue)
}
fieldValue.SetInt(intVal)
// If you need to handle other types (e.g. float, slice), add them here.
default:
return fmt.Errorf(
"unsupported field type '%s' for field '%s'",
fieldValue.Kind().String(), field.Name,
)
}
}
return nil
}
// isCredentialsPassword checks if the actualSegments match ["Credentials", "Password"]
// (case-insensitive).
func isCredentialsPassword(actualSegments []string) bool {
if len(actualSegments) < 2 {
return false
}
// e.g. last two items might be Credentials, Password
last := actualSegments[len(actualSegments)-1]
secondLast := actualSegments[len(actualSegments)-2]
return strings.EqualFold(secondLast, "Credentials") &&
strings.EqualFold(last, "Password")
}
// encryptPassword uses your existing utility functions to generate/retrieve a key
// and return an encrypted version of the supplied password.
func encryptPassword(plaintext string) (string, error) {
// Make sure a key exists
if err := utils.GenerateEncryptionKey(); err != nil {
// It's okay if the key already exists; that might not be a fatal error for you
logrus.Debugf("Encryption key might already exist: %v", err)
}
key, err := utils.GetEncryptionKey()
if err != nil {
return "", fmt.Errorf("failed to get encryption key: %w", err)
}
encrypted, err := utils.Encrypt(key, []byte(plaintext))
if err != nil {
return "", fmt.Errorf("failed to encrypt password: %w", err)
}
return encrypted, nil
}

View File

@ -0,0 +1,60 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cve
import (
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/views/cveallowlist/update"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func AddCveAllowlistCommand() *cobra.Command {
var opts update.UpdateView
cmd := &cobra.Command{
Use: "add",
Short: "Add cve allowlist",
Long: "Create allowlist of CVEs to ignore during vulnerability scanning",
Run: func(cmd *cobra.Command, args []string) {
var err error
updateView := &update.UpdateView{
CveId: opts.CveId,
IsExpire: opts.IsExpire,
ExpireDate: opts.ExpireDate,
}
err = updatecveView(updateView)
if err != nil {
log.Errorf("failed to add cveallowlist: %v", err)
}
},
}
flags := cmd.Flags()
flags.BoolVarP(&opts.IsExpire, "isexpire", "i", false, "Indicates whether the CVE entries should have an expiration date. Set to true to specify an expiration date")
flags.StringVarP(&opts.CveId, "cveid", "n", "", "Comma-separated list of CVE IDs to be added to the allowlist")
flags.StringVarP(&opts.ExpireDate, "expiredate", "d", "", "Specifies the expiration date for the CVE entries in the format 'YYYY-MM-DD'")
return cmd
}
func updatecveView(updateView *update.UpdateView) error {
if updateView == nil {
updateView = &update.UpdateView{}
}
update.UpdateCveView(updateView)
return api.UpdateSystemCve(*updateView)
}

View File

@ -0,0 +1,33 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cve
import (
"github.com/spf13/cobra"
)
func CVEAllowlist() *cobra.Command {
cmd := &cobra.Command{
Use: "cve-allowlist",
Short: "Manage system CVE allowlist",
Long: `Managing CVE lists that are intentionally excluded from vulnerability scanning`,
Example: `harbor cve-allowlist list`,
}
cmd.AddCommand(
ListCveCommand(),
AddCveAllowlistCommand(),
)
return cmd
}

View File

@ -0,0 +1,49 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cve
import (
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/cveallowlist/list"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func ListCveCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List system level allowlist of cve",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
cve, err := api.ListSystemCve()
if err != nil {
log.Fatalf("failed to get system cve list: %v", err)
}
FormatFlag := viper.GetString("output-format")
if FormatFlag != "" {
err = utils.PrintFormat(cve, FormatFlag)
if err != nil {
log.Fatalf("failed to print cve list: %v", err)
return
}
} else {
list.ListSystemCve(cve.Payload)
}
},
}
return cmd
}

View File

@ -15,8 +15,10 @@ package root
import (
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/health"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func HealthCommand() *cobra.Command {
@ -32,7 +34,15 @@ func HealthCommand() *cobra.Command {
if err != nil {
return err
}
health.PrintHealthStatus(status)
FormatFlag := viper.GetString("output-format")
if FormatFlag != "" {
err = utils.PrintFormat(status, FormatFlag)
if err != nil {
return err
}
} else {
health.PrintHealthStatus(status)
}
return nil
},
Example: ` # Get the health status of Harbor components`,

88
cmd/harbor/root/info.go Normal file
View File

@ -0,0 +1,88 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package root
import (
"fmt"
"github.com/goharbor/harbor-cli/cmd/harbor/internal/version"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/info/list"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// Lists the info of the Harbor system
func InfoCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "info",
Short: "Display detailed Harbor system, statistics, and CLI environment information",
Long: `The 'info' command retrieves and displays general information about the Harbor instance,
including system metadata, storage statistics, and CLI environment details such as user identity,
registry address, and CLI version.
The output can be formatted as table (default), JSON, or YAML using the '--output-format' flag.`,
Example: ` harbor info
harbor info --output-format json
harbor info -o yaml`,
RunE: func(cmd *cobra.Command, args []string) error {
var cliinfo *api.CLIInfo
var err error
generalInfo, err := api.GetSystemInfo()
if err != nil {
return err
}
stats, err := api.GetStats()
if err != nil {
return err
}
sysVolume, err := api.GetSystemVolumes()
if err != nil {
return err
}
cliVersion := version.Version
OSinfo := version.System
cliinfo, err = api.GetCLIInfo()
if err != nil {
return fmt.Errorf("Failed to get CLI info: %w", err)
}
systemInfo := list.CreateSystemInfo(
generalInfo.Payload,
stats.Payload,
sysVolume.Payload,
cliinfo,
cliVersion,
OSinfo,
)
FormatFlag := viper.GetString("output-format")
if FormatFlag != "" {
err = utils.PrintFormat(systemInfo, FormatFlag)
if err != nil {
log.Error(err)
}
} else {
list.ListInfo(&systemInfo)
}
return nil
},
}
return cmd
}

View File

@ -0,0 +1,31 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package instance
import "github.com/spf13/cobra"
func Instance() *cobra.Command {
cmd := &cobra.Command{
Use: "instance",
Short: "Manage preheat provider instances in Harbor",
Long: `Manage preheat provider instances used by Harbor for pre-distributing container images.
These instances represent external services such as Dragonfly or Kraken that help preheat images across nodes.`,
}
cmd.AddCommand(
CreateInstanceCommand(),
DeleteInstanceCommand(),
ListInstanceCommand(),
)
return cmd
}

View File

@ -0,0 +1,78 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package instance
import (
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/views/instance/create"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func CreateInstanceCommand() *cobra.Command {
var opts create.CreateView
cmd := &cobra.Command{
Use: "create",
Short: "Create a new preheat provider instance in Harbor",
Long: `Create a new preheat provider instance within Harbor for distributing container images.
The instance can be an external service such as Dragonfly, Kraken, or any custom provider.
You will need to provide the instance's name, vendor, endpoint, and optionally other details such as authentication and security options.`,
Example: ` harbor-cli instance create --name my-instance --provider Dragonfly --url http://dragonfly.local --description "My preheat provider instance" --enable=true`,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
var err error
createView := &create.CreateView{
Name: opts.Name,
Vendor: opts.Vendor,
Description: opts.Description,
Endpoint: opts.Endpoint,
Insecure: opts.Insecure,
Enabled: opts.Enabled,
AuthMode: opts.AuthMode,
AuthInfo: opts.AuthInfo,
}
if opts.Name != "" && opts.Vendor != "" && opts.Endpoint != "" {
err = api.CreateInstance(opts)
} else {
err = createInstanceView(createView)
}
if err != nil {
log.Errorf("failed to create instance: %v", err)
}
},
}
flags := cmd.Flags()
flags.StringVarP(&opts.Name, "name", "n", "", "Name of the instance")
flags.StringVarP(&opts.Vendor, "provider", "p", "", "Provider for the instance")
flags.StringVarP(&opts.Endpoint, "url", "u", "", "URL for the instance")
flags.StringVarP(&opts.Description, "description", "", "", "Description of the instance")
flags.BoolVarP(&opts.Insecure, "insecure", "i", true, "Whether or not the certificate will be verified when Harbor tries to access the server")
flags.BoolVarP(&opts.Enabled, "enable", "", true, "Whether it is enabled or not")
flags.StringVarP(&opts.AuthMode, "authmode", "a", "NONE", "Choosing different types of authentication method")
return cmd
}
func createInstanceView(createView *create.CreateView) error {
if createView == nil {
createView = &create.CreateView{}
}
create.CreateInstanceView(createView)
return api.CreateInstance(*createView)
}

View File

@ -0,0 +1,56 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package instance
import (
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func DeleteInstanceCommand() *cobra.Command {
var instanceID int64
cmd := &cobra.Command{
Use: "delete",
Short: "Delete a preheat provider instance by its name or ID",
Long: `Delete a preheat provider instance from Harbor. You can specify the instance name or ID directly as an argument.
If no argument is provided, you will be prompted to select an instance from a list of available instances.`,
Example: ` harbor-cli instance delete my-instance
harbor-cli instance delete 12345`,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
var err error
var instanceName string
if instanceID != -1 {
instanceName, err = api.GetInstanceNameByID(instanceID)
if err != nil {
log.Errorf("%v", err)
return
}
} else if len(args) > 0 {
instanceName = args[0]
} else {
instanceName = prompt.GetInstanceFromUser()
}
err = api.DeleteInstance(instanceName)
if err != nil {
log.Errorf("failed to delete instance: %v", err)
}
},
}
cmd.Flags().Int64VarP(&instanceID, "id", "i", -1, "ID of the instance to delete")
return cmd
}

View File

@ -0,0 +1,61 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package instance
import (
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/instance/list"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func ListInstanceCommand() *cobra.Command {
var opts api.ListFlags
cmd := &cobra.Command{
Use: "list",
Short: "List all preheat provider instances in Harbor",
Long: `List all preheat provider instances registered in Harbor. You can paginate the results,
filter them using a query string, and sort them in ascending or descending order.
This command provides an easy way to view all instances along with their details.`,
Example: ` harbor-cli instance list --page 1 --page-size 10
harbor-cli instance list --query "name=my-instance" --sort "asc"`,
Run: func(cmd *cobra.Command, args []string) {
instance, err := api.ListInstance(opts)
if err != nil {
log.Fatalf("failed to get instance list: %v", err)
}
FormatFlag := viper.GetString("output-format")
if FormatFlag != "" {
err = utils.PrintFormat(instance, FormatFlag)
if err != nil {
log.Errorf("Failed to print config: %v", err)
}
} else {
list.ListInstance(instance.Payload)
}
},
}
flags := cmd.Flags()
flags.Int64VarP(&opts.Page, "page", "", 1, "Page number")
flags.Int64VarP(&opts.PageSize, "page-size", "", 10, "Size of per page")
flags.StringVarP(&opts.Q, "query", "q", "", "Query string to query resources")
flags.StringVarP(&opts.Sort, "sort", "", "", "Sort the resource list in ascending or descending order")
return cmd
}

View File

@ -28,7 +28,7 @@ func CreateLabelCommand() *cobra.Command {
Short: "create label",
Long: "create label in harbor",
Example: "harbor label create",
Args: cobra.NoArgs,
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
var err error
createView := &create.CreateView{

View File

@ -14,10 +14,12 @@
package labels
import (
"fmt"
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
log "github.com/sirupsen/logrus"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/spf13/cobra"
)
@ -28,22 +30,27 @@ func DeleteLabelCommand() *cobra.Command {
Short: "delete label",
Example: "harbor label delete [labelname]",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
var err error
var labelId int64
deleteView := &api.ListFlags{
Scope: opts.Scope,
}
if len(args) > 0 {
labelId, _ := api.GetLabelIdByName(args[0])
err = api.DeleteLabel(labelId)
labelId, _ = api.GetLabelIdByName(args[0])
} else {
labelId := prompt.GetLabelIdFromUser(*deleteView)
err = api.DeleteLabel(labelId)
labelList, err := api.ListLabel(*deleteView)
if err != nil {
return fmt.Errorf("failed to get label list: %v", utils.ParseHarborErrorMsg(err))
}
labelId = prompt.GetLabelIdFromUser(labelList.Payload)
}
err = api.DeleteLabel(labelId)
if err != nil {
log.Errorf("failed to delete label: %v", err)
return fmt.Errorf("failed to delete label: %v", utils.ParseHarborErrorMsg(err))
}
return nil
},
}
flags := cmd.Flags()

View File

@ -14,6 +14,8 @@
package labels
import (
"fmt"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/label/list"
@ -28,20 +30,30 @@ func ListLabelCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "list labels",
Run: func(cmd *cobra.Command, args []string) {
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
if opts.PageSize > 100 {
return fmt.Errorf("page size should be less than or equal to 100")
}
label, err := api.ListLabel(opts)
if err != nil {
log.Fatalf("failed to get label list: %v", err)
}
FormatFlag := viper.GetString("output-format")
if FormatFlag != "" {
err = utils.PrintFormat(label, FormatFlag)
if len(label.Payload) == 0 {
log.Info("No labels found")
return nil
}
formatFlag := viper.GetString("output-format")
if formatFlag != "" {
err = utils.PrintFormat(label, formatFlag)
if err != nil {
log.Error(err)
}
} else {
list.ListLabels(label.Payload)
}
return nil
},
}

View File

@ -40,7 +40,12 @@ func UpdateLableCommand() *cobra.Command {
if len(args) > 0 {
labelId, err = api.GetLabelIdByName(args[0])
} else {
labelId = prompt.GetLabelIdFromUser(updateflags)
labelList, err := api.ListLabel(updateflags)
if err != nil {
log.Errorf("failed to get label list: %v", err)
return
}
labelId = prompt.GetLabelIdFromUser(labelList.Payload)
}
if err != nil {
log.Errorf("failed to parse label id: %v", err)

View File

@ -64,28 +64,20 @@ func LoginCommand() *cobra.Command {
Name: Name,
}
// autogenerate name
if loginView.Name == "" && loginView.Server != "" && loginView.Username != "" {
loginView.Name = fmt.Sprintf("%s@%s", loginView.Username, utils.SanitizeServerAddress(loginView.Server))
}
var err error
if loginView.Server != "" && loginView.Username != "" && loginView.Password != "" {
err = runLogin(loginView)
} else {
err = createLoginView(&loginView)
}
var config *utils.HarborConfig
config, err = utils.GetCurrentHarborConfig()
if err != nil {
return err
return fmt.Errorf("failed to get current harbor config: %s", err)
}
if err := ProcessLogin(loginView, config); err != nil {
return fmt.Errorf("login failed: %w", err)
}
return nil
},
}
flags := cmd.Flags()
flags.StringVarP(&Name, "name", "", "", "name for the set of credentials")
flags.StringVarP(&Username, "username", "u", "", "Username")
flags.StringVarP(&Password, "password", "p", "", "Password")
flags.BoolVar(&passwordStdin, "password-stdin", false, "Take the password from stdin")
@ -93,7 +85,23 @@ func LoginCommand() *cobra.Command {
return cmd
}
func createLoginView(loginView *login.LoginView) error {
// ProcessLogin applies a simplified decision logic to run login or launch an interactive view.
func ProcessLogin(loginView login.LoginView, config *utils.HarborConfig) error {
// Auto-generate the name
loginView.Name = fmt.Sprintf("%s@%s", loginView.Username, utils.SanitizeServerAddress(loginView.Server))
// If complete credentials are provided (overrides), run login using them directly.
if loginView.Server != "" && loginView.Username != "" && loginView.Password != "" {
return RunLogin(loginView)
}
// If nothing matches, launch the interactive view.
return CreateLoginView(&loginView)
}
// CreateLoginView launches the interactive login view.
// In this implementation, it calls login.CreateView and then tries to run login.
func CreateLoginView(loginView *login.LoginView) error {
if loginView == nil {
loginView = &login.LoginView{
Server: "",
@ -104,10 +112,11 @@ func createLoginView(loginView *login.LoginView) error {
}
login.CreateView(loginView)
return runLogin(*loginView)
return RunLogin(*loginView)
}
func runLogin(opts login.LoginView) error {
// RunLogin attempts to log in using the provided LoginView credentials.
func RunLogin(opts login.LoginView) error {
opts.Server = utils.FormatUrl(opts.Server)
clientConfig := &harbor.ClientSetConfig{
@ -115,18 +124,36 @@ func runLogin(opts login.LoginView) error {
Username: opts.Username,
Password: opts.Password,
}
client := utils.GetClientByConfig(clientConfig)
ctx := context.Background()
_, err := client.User.GetCurrentUserInfo(ctx, &user.GetCurrentUserInfoParams{})
err := utils.ValidateURL(opts.Server)
if err != nil {
return fmt.Errorf("login failed, please check your credentials: %s", err)
return fmt.Errorf("invalid server URL: %s", err)
}
client := utils.GetClientByConfig(clientConfig)
ctx := context.Background()
_, err = client.User.GetCurrentUserInfo(ctx, &user.GetCurrentUserInfoParams{})
if err != nil {
return fmt.Errorf("%v", utils.ParseHarborErrorMsg(err))
}
if err := utils.GenerateEncryptionKey(); err != nil {
fmt.Println("Encryption key already exists or could not be created:", err)
}
key, err := utils.GetEncryptionKey()
if err != nil {
fmt.Println("Error getting encryption key:", err)
return fmt.Errorf("failed to get encryption key: %s", err)
}
encryptedPassword, err := utils.Encrypt(key, []byte(opts.Password))
if err != nil {
fmt.Println("Error encrypting password:", err)
return fmt.Errorf("failed to encrypt password: %s", err)
}
cred := utils.Credential{
Name: opts.Name,
Username: opts.Username,
Password: opts.Password,
Password: encryptedPassword,
ServerAddress: opts.Server,
}
harborData, err := utils.GetCurrentHarborData()
@ -138,14 +165,16 @@ func runLogin(opts login.LoginView) error {
existingCred, err := utils.GetCredentials(opts.Name)
if err == nil {
if existingCred.Username == opts.Username && existingCred.ServerAddress == opts.Server {
if existingCred.Password == opts.Password {
if existingCred.Password == encryptedPassword {
log.Warn("Credentials already exist in the config file. They were not added again.")
fmt.Printf("Login successful for %s at %s\n", opts.Username, opts.Server)
return nil
} else {
log.Warn("Credentials already exist in the config file but the password is different. Updating the password.")
if err = utils.UpdateCredentialsInConfigFile(cred, configPath); err != nil {
log.Fatalf("failed to update the credential: %s", err)
}
fmt.Printf("Login successful for %s at %s\n", opts.Username, opts.Server)
return nil
}
} else {
@ -153,6 +182,7 @@ func runLogin(opts login.LoginView) error {
if err = utils.UpdateCredentialsInConfigFile(cred, configPath); err != nil {
log.Fatalf("failed to update the credential: %s", err)
}
fmt.Printf("Login successful for %s at %s\n", opts.Username, opts.Server)
return nil
}
}
@ -161,5 +191,6 @@ func runLogin(opts login.LoginView) error {
return fmt.Errorf("failed to store the credential: %s", err)
}
log.Debugf("Credentials successfully added to the config file.")
fmt.Printf("Login successful for %s at %s\n", opts.Username, opts.Server)
return nil
}

View File

@ -11,26 +11,26 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package e2e
package root_test
import (
"testing"
"github.com/goharbor/harbor-cli/cmd/harbor/root"
helpers "github.com/goharbor/harbor-cli/test/helper"
"github.com/stretchr/testify/assert"
)
func Test_Login_Success(t *testing.T) {
tempDir := t.TempDir()
data := Initialize(t, tempDir)
defer ConfigCleanup(t, data)
data := helpers.Initialize(t, tempDir)
defer helpers.ConfigCleanup(t, data)
cmd := root.LoginCommand()
validServerAddresses := []string{
"http://demo.goharbor.io:80",
"https://demo.goharbor.io:443",
"http://demo.goharbor.io",
"https://demo.goharbor.io",
// "demo.goharbor.io",
}
for _, serverAddress := range validServerAddresses {
@ -38,7 +38,6 @@ func Test_Login_Success(t *testing.T) {
args := []string{serverAddress}
cmd.SetArgs(args)
assert.NoError(t, cmd.Flags().Set("name", "test"))
assert.NoError(t, cmd.Flags().Set("username", "harbor-cli"))
assert.NoError(t, cmd.Flags().Set("password", "Harbor12345"))
@ -50,13 +49,12 @@ func Test_Login_Success(t *testing.T) {
func Test_Login_Failure_WrongServer(t *testing.T) {
tempDir := t.TempDir()
data := Initialize(t, tempDir)
defer ConfigCleanup(t, data)
data := helpers.Initialize(t, tempDir)
defer helpers.ConfigCleanup(t, data)
cmd := root.LoginCommand()
cmd.SetArgs([]string{"wrongserver"})
assert.NoError(t, cmd.Flags().Set("name", "test"))
assert.NoError(t, cmd.Flags().Set("username", "harbor-cli"))
assert.NoError(t, cmd.Flags().Set("password", "Harbor12345"))
@ -66,13 +64,12 @@ func Test_Login_Failure_WrongServer(t *testing.T) {
func Test_Login_Failure_WrongUsername(t *testing.T) {
tempDir := t.TempDir()
data := Initialize(t, tempDir)
defer ConfigCleanup(t, data)
data := helpers.Initialize(t, tempDir)
defer helpers.ConfigCleanup(t, data)
cmd := root.LoginCommand()
cmd.SetArgs([]string{"http://demo.goharbor.io"})
assert.NoError(t, cmd.Flags().Set("name", "test"))
assert.NoError(t, cmd.Flags().Set("username", "does-not-exist"))
assert.NoError(t, cmd.Flags().Set("password", "Harbor12345"))
@ -82,13 +79,12 @@ func Test_Login_Failure_WrongUsername(t *testing.T) {
func Test_Login_Failure_WrongPassword(t *testing.T) {
tempDir := t.TempDir()
data := Initialize(t, tempDir)
defer ConfigCleanup(t, data)
data := helpers.Initialize(t, tempDir)
defer helpers.ConfigCleanup(t, data)
cmd := root.LoginCommand()
cmd.SetArgs([]string{"http://demo.goharbor.io"})
assert.NoError(t, cmd.Flags().Set("name", "test"))
assert.NoError(t, cmd.Flags().Set("username", "admin"))
assert.NoError(t, cmd.Flags().Set("password", "wrong"))

217
cmd/harbor/root/logs.go Normal file
View File

@ -0,0 +1,217 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package root
import (
"fmt"
"os"
"strings"
"time"
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/utils"
list "github.com/goharbor/harbor-cli/pkg/views/logs"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var logsLogger = log.New()
func Logs() *cobra.Command {
var opts api.ListFlags
var follow bool
var refreshInterval string
cmd := &cobra.Command{
Use: "logs",
Short: "Get recent logs of the projects which the user is a member of",
Args: cobra.NoArgs,
Long: `Get recent logs of the projects which the user is a member of.
This command retrieves the audit logs for the projects the user is a member of. It supports pagination, sorting, and filtering through query parameters. The logs can be followed in real-time with the --follow flag, and the output can be formatted as JSON with the --output-format flag.
harbor-cli logs --page 1 --page-size 10 --query "operation=push" --sort "op_time:desc"
harbor-cli logs --follow --refresh-interval 2s
harbor-cli logs --output-format json`,
Run: func(cmd *cobra.Command, args []string) {
if refreshInterval != "" && !follow {
fmt.Println("The --refresh-interval flag is only applicable when using --follow. It will be ignored.")
}
if follow {
var interval time.Duration = 5 * time.Second
var err error
if refreshInterval != "" {
interval, err = time.ParseDuration(refreshInterval)
if err != nil {
log.Fatalf("invalid refresh interval: %v", err)
}
}
followLogs(opts, interval)
} else {
logs, err := api.AuditLogs(opts)
if err != nil {
log.Fatalf("failed to retrieve audit logs: %v", err)
}
formatFlag := viper.GetString("output-format")
if formatFlag != "" {
log.WithField("output_format", formatFlag).Debug("Output format selected")
err = utils.PrintFormat(logs.Payload, formatFlag)
if err != nil {
return
}
} else {
list.ListLogs(logs.Payload)
}
}
},
}
flags := cmd.Flags()
flags.Int64VarP(&opts.Page, "page", "", 1, "Page number")
flags.Int64VarP(&opts.PageSize, "page-size", "", 10, "Size of per page")
flags.StringVarP(&opts.Q, "query", "q", "", "Query string to query resources")
flags.StringVarP(
&opts.Sort,
"sort",
"",
"",
"Sort the resource list in ascending or descending order",
)
flags.BoolVarP(&follow, "follow", "f", false, "Follow log output (tail -f behavior)")
flags.StringVarP(&refreshInterval, "refresh-interval", "n", "",
"Interval to refresh logs when following (default: 5s)")
return cmd
}
func followLogs(opts api.ListFlags, interval time.Duration) {
var lastLogTime *time.Time
logsLogger.SetFormatter(&log.TextFormatter{
FullTimestamp: true,
TimestampFormat: "2006-01-02 15:04:05",
DisableColors: false,
})
logsLogger.SetLevel(log.InfoLevel)
logsLogger.SetOutput(os.Stdout)
fmt.Println("Following Harbor audit logs... (Press Ctrl+C to stop)")
for {
logs, err := api.AuditLogs(opts)
if err != nil {
log.Errorf("failed to retrieve audit logs: %v", err)
time.Sleep(interval)
continue
}
var newLogs []*models.AuditLogExt
if lastLogTime != nil {
for _, logEntry := range logs.Payload {
logTime := time.Time(logEntry.OpTime)
if !logTime.IsZero() && logTime.After(*lastLogTime) {
newLogs = append(newLogs, logEntry)
}
}
} else {
newLogs = logs.Payload
}
if len(logs.Payload) > 0 {
logTime := time.Time(logs.Payload[0].OpTime)
if !logTime.IsZero() {
lastLogTime = &logTime
}
}
printLogsAsStream(newLogs)
time.Sleep(interval)
}
}
func printLogsAsStream(logs []*models.AuditLogExt) {
for _, logEntry := range logs {
logTime := time.Time(logEntry.OpTime)
level := getLogLevel(logEntry.OperationResult)
displayUser := truncateUsername(logEntry.Username)
resource := getResourceInfo(logEntry.ResourceType, logEntry.Resource)
resultIcon := "✓"
if !logEntry.OperationResult {
resultIcon = "✗"
}
message := fmt.Sprintf("%s %s %s %s",
displayUser,
logEntry.Operation,
resource,
resultIcon)
entry := logsLogger.WithTime(logTime)
switch level {
case "error":
entry.Error(message)
case "info":
entry.Info(message)
default:
entry.Debug(message)
}
}
}
func truncateUsername(username string) string {
if username == "" {
return "unknown"
}
if len(username) > 30 {
if parts := strings.Split(username, "+"); len(parts) > 1 {
project := strings.TrimPrefix(parts[0], "robt_")
return fmt.Sprintf("%s+robot", project)
}
return username[:27] + "..."
}
return username
}
func getLogLevel(operationResult bool) string {
switch operationResult {
case false:
return "error"
case true:
return "info"
default:
return "error"
}
}
func getResourceInfo(resourceType, resource string) string {
if resourceType == "" && resource == "" {
return "unknown"
}
if resourceType != "" && resource != "" {
return fmt.Sprintf("%s:%s", resourceType, resource)
}
if resourceType != "" {
return resourceType
}
return resource
}

View File

@ -14,6 +14,7 @@
package project
import (
"github.com/goharbor/harbor-cli/cmd/harbor/root/project/config"
"github.com/spf13/cobra"
)
@ -30,7 +31,9 @@ func Project() *cobra.Command {
ListProjectCommand(),
ViewCommand(),
LogsProjectCommmand(),
config.ProjectConfigCommand(),
SearchProjectCommand(),
Robot(),
)
return cmd

View File

@ -0,0 +1,32 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import "github.com/spf13/cobra"
var isID bool
func ProjectConfigCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "config",
Short: "Manage project configuration",
}
cmd.AddCommand(
UpdateProjectConfigCmd(),
ListProjectConfigCmd(),
)
cmd.PersistentFlags().BoolVarP(&isID, "id", "", false, "Use project ID instead of name")
return cmd
}

View File

@ -0,0 +1,81 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"fmt"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/project/config/list"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func ListProjectConfigCmd() *cobra.Command {
var err error
var projectNameorID string
cmd := &cobra.Command{
Use: "list [project_name]",
Short: "List configuration of a Harbor project by name or ID",
Long: `Display the configuration metadata of a Harbor project specified by its name or ID.
If no project name or ID is provided as an argument, you will be prompted to select a project interactively.
You can use the global flag '--output-format' to specify the output format, e.g. 'json' or 'yaml', for machine-readable output.
Examples:
# List configuration of project 'myproject' by name
harbor-cli project config list myproject
# List configuration of project with ID '123'
harbor-cli project config list 123
# Run interactively (prompt to select project)
harbor-cli project config list
# List config in JSON format
harbor-cli project config list myproject --output-format json
`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
projectNameorID, err = prompt.GetProjectNameFromUser()
if err != nil {
return fmt.Errorf("failed to get project name: %v", err)
}
isID = false
} else {
projectNameorID = args[0]
}
response, err := api.ListConfig(isID, projectNameorID)
if err != nil {
return fmt.Errorf("failed to list metadata: %v", utils.ParseHarborErrorMsg(err))
}
formatFlag := viper.GetString("output-format")
if formatFlag != "" {
err = utils.PrintFormat(response.Payload, formatFlag)
if err != nil {
return err
}
} else {
list.ListConfig(response.Payload)
}
return nil
},
}
return cmd
}

View File

@ -0,0 +1,172 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"fmt"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/project/config/update"
"github.com/spf13/cobra"
)
var (
publicFlag string
autoScanFlag string
preventVulFlag string
reuseSysCVEFlag string
enableContentTrustFlag string
enableContentTrustCosignFlag string
severityFlag string
)
func UpdateProjectConfigCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "update [project_name]",
Short: "Interactively or via flags update project configuration in Harbor",
Long: `Update the configuration settings of a Harbor project either interactively or directly using command-line flags.
You can specify the project by its name or ID as an argument. If not provided, you will be prompted to select a project interactively.
Examples:
# Update project 'myproject' visibility to public
harbor-cli project config update myproject --public true
# Update multiple settings in one command
harbor-cli project config update myproject --public false --prevent-vul true --severity high
# Run interactively without flags
harbor-cli project config update
Supported flag values:
- Boolean flags (public, auto-scan, prevent-vul, reuse-sys-cve-allowlist, enable-content-trust, enable-content-trust-cosign): "true" or "false"
- Severity: one of "low", "medium", "high", "critical"
`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var err error
var projectIDOrName string
if len(args) > 0 {
projectIDOrName = args[0]
} else {
projectIDOrName, err = prompt.GetProjectNameFromUser()
if err != nil {
return fmt.Errorf("Failed to get project name: %v", err)
}
isID = false
}
resp, err := api.GetProject(projectIDOrName, isID)
if err != nil {
return fmt.Errorf("Failed to list project config: %v", utils.ParseHarborErrorMsg(err))
}
conf := resp.Payload.Metadata
flags := cmd.Flags()
flagsUsed := false
if flags.Changed("public") {
if err := validateFlag("public", publicFlag); err != nil {
return err
}
conf.Public = publicFlag
flagsUsed = true
}
if flags.Changed("auto-scan") {
if err := validateFlag("auto-scan", autoScanFlag); err != nil {
return err
}
conf.AutoScan = &autoScanFlag
flagsUsed = true
}
if flags.Changed("prevent-vul") {
if err := validateFlag("prevent-vul", preventVulFlag); err != nil {
return err
}
conf.PreventVul = &preventVulFlag
flagsUsed = true
}
if flags.Changed("reuse-sys-cve") {
if err := validateFlag("reuse-sys-cve", reuseSysCVEFlag); err != nil {
return err
}
conf.ReuseSysCVEAllowlist = &reuseSysCVEFlag
flagsUsed = true
}
if flags.Changed("enable-content-trust") {
if err := validateFlag("enable-content-trust", enableContentTrustFlag); err != nil {
return err
}
conf.EnableContentTrust = &enableContentTrustFlag
flagsUsed = true
}
if flags.Changed("enable-content-trust-cosign") {
if err := validateFlag("enable-content-trust-cosign", enableContentTrustCosignFlag); err != nil {
return err
}
conf.EnableContentTrustCosign = &enableContentTrustCosignFlag
flagsUsed = true
}
if flags.Changed("severity") {
if err := validateFlag("severity", severityFlag); err != nil {
return err
}
conf.Severity = &severityFlag
flagsUsed = true
}
if !flagsUsed {
update.UpdateProjectMetadataView(conf)
}
err = api.UpdateConfig(isID, projectIDOrName, *conf)
if err != nil {
return fmt.Errorf("Failed to update project config: %v", utils.ParseHarborErrorMsg(err))
}
fmt.Printf("Project %s configuration updated successfully.\n", projectIDOrName)
return nil
},
}
flags := cmd.Flags()
flags.StringVar(&publicFlag, "public", "", "Set project visibility (true/false)")
flags.StringVar(&autoScanFlag, "auto-scan", "", "Enable or disable auto scan (true/false)")
flags.StringVar(&preventVulFlag, "prevent-vul", "", "Enable or disable vulnerability prevention (true/false)")
flags.StringVar(&reuseSysCVEFlag, "reuse-sys-cve", "", "Enable or disable reuse of system CVE allowlist (true/false)")
flags.StringVar(&enableContentTrustFlag, "enable-content-trust", "", "Enable or disable content trust (true/false)")
flags.StringVar(&enableContentTrustCosignFlag, "enable-content-trust-cosign", "", "Enable or disable content trust cosign (true/false)")
flags.StringVar(&severityFlag, "severity", "", "Set severity level")
return cmd
}
func validateFlag(flagName, flagValue string) error {
allowed := map[string]bool{
"low": true,
"medium": true,
"high": true,
"critical": true,
}
if flagName == "severity" && !allowed[flagValue] {
return fmt.Errorf("Invalid value for --%s: %s. Allowed values are: low, medium, high, critical", flagName, flagValue)
}
if flagName != "severity" && flagValue != "true" && flagValue != "false" {
return fmt.Errorf("Invalid value for --%s: %s. Expected 'true' or 'false'", flagName, flagValue)
}
return nil
}

View File

@ -14,7 +14,10 @@
package project
import (
"fmt"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/project/create"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@ -28,32 +31,51 @@ func CreateProjectCommand() *cobra.Command {
Use: "create [project name]",
Short: "create project",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
var err error
createView := &create.CreateView{
ProjectName: opts.ProjectName,
Public: opts.Public,
RegistryID: opts.RegistryID,
StorageLimit: opts.StorageLimit,
ProxyCache: false,
}
var ProjectName string
if len(args) > 0 {
opts.ProjectName = args[0]
}
if opts.ProxyCache && opts.RegistryID == "" {
return fmt.Errorf("proxy cache selected but no registry ID provided. Use --registry-id")
}
if !opts.ProxyCache && opts.RegistryID != "" {
return fmt.Errorf("registry ID should only be provided when proxy-cache is enabled")
}
if opts.ProjectName != "" && opts.StorageLimit != "" {
log.Debug("Attempting to create project using flags...")
err = api.CreateProject(opts)
ProjectName = opts.ProjectName
} else {
log.Debug("Switching to interactive view...")
createView := &create.CreateView{
ProjectName: opts.ProjectName,
Public: opts.Public,
RegistryID: opts.RegistryID,
StorageLimit: opts.StorageLimit,
ProxyCache: opts.ProxyCache,
}
err = createProjectView(createView)
ProjectName = createView.ProjectName
}
if err != nil {
log.Errorf("failed to create project: %v", err)
return fmt.Errorf("failed to create project: %v", utils.ParseHarborErrorMsg(err))
}
},
}
fmt.Printf("Project '%s' created successfully\n", ProjectName)
return nil
}}
flags := cmd.Flags()
flags.BoolVarP(&opts.Public, "public", "", false, "Project is public or private")
flags.StringVarP(&opts.RegistryID, "registry-id", "", "", "ID of referenced registry when creating the proxy cache project")
flags.StringVarP(&opts.StorageLimit, "storage-limit", "", "-1", "Storage quota of the project")
flags.StringVarP(&opts.StorageLimit, "storage-limit", "", "", "Storage quota of the project")
flags.BoolVarP(&opts.ProxyCache, "proxy-cache", "", false, "Whether the project is a proxy cache project")
return cmd
@ -69,7 +91,9 @@ func createProjectView(createView *create.CreateView) error {
}
}
create.CreateProjectView(createView)
err := create.CreateProjectView(createView)
if err != nil {
return err
}
return api.CreateProject(*createView)
}

View File

@ -14,8 +14,13 @@
package project
import (
"fmt"
"strconv"
"sync"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
@ -23,28 +28,107 @@ import (
// DeleteProjectCommand creates a new `harbor delete project` command
func DeleteProjectCommand() *cobra.Command {
var forceDelete bool
var projectID string
cmd := &cobra.Command{
Use: "delete",
Short: "delete project by name or id",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
var err error
Use: "delete",
Short: "Delete project by name or ID",
Example: "harbor project delete [projectname1] [projectname2] or harbor project delete --project-id [projectid]",
Long: "Delete project by name or ID. Multiple projects can be deleted by providing their names as arguments. If no arguments are provided, it will prompt for the project name. Use --project-id to specify the project ID for single project directly. The --force flag will delete all repositories and artifacts within the project.",
Args: cobra.MinimumNArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
var wg sync.WaitGroup
var mu sync.Mutex
if len(args) > 0 {
err = api.DeleteProject(args[0], forceDelete)
successfulDeletes := []string{}
failedDeletes := map[string]string{}
if projectID != "" {
if len(args) > 0 {
return fmt.Errorf("--project-id cannot be used with additional arguments")
}
if _, err := strconv.Atoi(projectID); err != nil {
return fmt.Errorf("--project-id must be a numeric value")
}
}
if projectID != "" {
log.Debugf("Deleting project with ID: %s", projectID)
wg.Add(1)
go func(id string) {
defer wg.Done()
if err := api.DeleteProject(id, forceDelete, true); err != nil {
mu.Lock()
failedDeletes[id] = utils.ParseHarborErrorMsg(err)
mu.Unlock()
} else {
mu.Lock()
successfulDeletes = append(successfulDeletes, id)
mu.Unlock()
}
}(projectID)
} else if len(args) > 0 {
// Delete by project name from args
log.Debugf("Deleting %d projects from args...", len(args))
for _, projectName := range args {
pn := projectName
log.Debugf("Initiating delete for project: %s", pn)
wg.Add(1)
go func(projectName string) {
defer wg.Done()
log.Debugf("Deleting project '%s' with force=%v", projectName, forceDelete)
if err := api.DeleteProject(projectName, forceDelete, false); err != nil {
mu.Lock()
failedDeletes[projectName] = utils.ParseHarborErrorMsg(err)
mu.Unlock()
} else {
mu.Lock()
successfulDeletes = append(successfulDeletes, projectName)
mu.Unlock()
}
}(pn)
}
} else {
projectName := prompt.GetProjectNameFromUser()
err = api.DeleteProject(projectName, forceDelete)
// If no arguments provided, prompt user for project name
log.Debug("No arguments provided. Prompting user for project name.")
projectName, err := prompt.GetProjectNameFromUser()
if err != nil {
return fmt.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
}
log.Debugf("User input project: %s", projectName)
log.Debugf("Deleting project '%s' with force=%v", projectName, forceDelete)
if err := api.DeleteProject(projectName, forceDelete, false); err != nil {
return fmt.Errorf("failed to delete project: %v", utils.ParseHarborErrorMsg(err))
}
fmt.Printf("Project '%s' deleted successfully\n", projectName)
return nil
}
if err != nil {
log.Errorf("failed to delete project: %v", err)
wg.Wait()
if len(successfulDeletes) > 0 {
fmt.Println("Successfully deleted projects:")
for _, name := range successfulDeletes {
fmt.Printf(" - %s\n", name)
}
}
if len(failedDeletes) > 0 {
fmt.Println("Failed to delete projects:")
for name, reason := range failedDeletes {
fmt.Printf(" - %s: %s\n", name, reason)
}
return fmt.Errorf("failed to delete %d project(s)", len(failedDeletes))
}
log.Debug("All requested projects deleted successfully.")
return nil
},
}
flags := cmd.Flags()
flags.BoolVar(&forceDelete, "force", false, "Deletes all repositories and artifacts within the project")
flags.BoolVar(&forceDelete, "force", false, "Forcefully delete all repositories, artifacts, and policies in the project. Use with extreme caution—this action is irreversible.")
flags.StringVar(&projectID, "project-id", "", "Specify project ID instead of project name")
return cmd
}

View File

@ -14,7 +14,10 @@
package project
import (
"fmt"
"github.com/goharbor/go-client/pkg/sdk/v2.0/client/project"
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/utils"
list "github.com/goharbor/harbor-cli/pkg/views/project/list"
@ -27,44 +30,67 @@ func ListProjectCommand() *cobra.Command {
var opts api.ListFlags
var private bool
var public bool
var projects project.ListProjectsOK
var allProjects []*models.Project
var err error
cmd := &cobra.Command{
Use: "list",
Short: "list project",
Run: func(cmd *cobra.Command, args []string) {
if private && public {
log.Fatal("Cannot specify both --private and --public flags")
} else if private {
opts.Public = false
projects, err = api.ListProject(opts)
} else if public {
opts.Public = true
projects, err = api.ListProject(opts)
} else {
projects, err = api.ListAllProjects(opts)
Short: "List projects",
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
log.Debug("Starting project list command")
if opts.PageSize > 100 {
return fmt.Errorf("page size should be less than or equal to 100")
}
if err != nil {
log.Fatalf("failed to get projects list: %v", err)
return
if private && public {
return fmt.Errorf("Cannot specify both --private and --public flags")
}
FormatFlag := viper.GetString("output-format")
if FormatFlag != "" {
err = utils.PrintFormat(projects, FormatFlag)
var listFunc func(...api.ListFlags) (project.ListProjectsOK, error)
if private {
log.Debug("Using private project list function")
opts.Public = false
listFunc = api.ListProject
} else if public {
log.Debug("Using public project list function")
opts.Public = true
listFunc = api.ListProject
} else {
log.Debug("Using list all projects function")
listFunc = api.ListAllProjects
}
log.Debug("Fetching projects...")
allProjects, err = fetchProjects(listFunc, opts)
if err != nil {
return fmt.Errorf("failed to get projects list: %v", utils.ParseHarborErrorMsg(err))
}
log.WithField("count", len(allProjects)).Debug("Number of projects fetched")
if len(allProjects) == 0 {
log.Info("No projects found")
return nil
}
formatFlag := viper.GetString("output-format")
if formatFlag != "" {
log.WithField("output_format", formatFlag).Debug("Output format selected")
err = utils.PrintFormat(allProjects, formatFlag)
if err != nil {
log.Error(err)
return err
}
} else {
list.ListProjects(projects.Payload)
log.Debug("Listing projects using default view")
list.ListProjects(allProjects)
}
return nil
},
}
flags := cmd.Flags()
flags.StringVarP(&opts.Name, "name", "", "", "Name of the project")
flags.Int64VarP(&opts.Page, "page", "", 1, "Page number")
flags.Int64VarP(&opts.PageSize, "page-size", "", 10, "Size of per page")
flags.Int64VarP(&opts.PageSize, "page-size", "", 0, "Size of per page (0 to fetch all)")
flags.BoolVarP(&private, "private", "", false, "Show only private projects")
flags.BoolVarP(&public, "public", "", false, "Show only public projects")
flags.StringVarP(&opts.Q, "query", "q", "", "Query string to query resources")
@ -72,3 +98,47 @@ func ListProjectCommand() *cobra.Command {
return cmd
}
func fetchProjects(listFunc func(...api.ListFlags) (project.ListProjectsOK, error), opts api.ListFlags) ([]*models.Project, error) {
var allProjects []*models.Project
if opts.PageSize == 0 {
log.Debug("Page size is 0, will fetch all pages")
opts.PageSize = 100
opts.Page = 1
for {
log.WithFields(log.Fields{
"page": opts.Page,
"page_size": opts.PageSize,
}).Debug("Fetching next page of projects")
projects, err := listFunc(opts)
if err != nil {
return nil, err
}
log.WithField("fetched_count", len(projects.Payload)).Debug("Fetched projects from current page")
allProjects = append(allProjects, projects.Payload...)
if len(projects.Payload) < int(opts.PageSize) {
log.Debug("Last page reached, stopping pagination")
break
}
opts.Page++
}
} else {
log.WithFields(log.Fields{
"page": opts.Page,
"page_size": opts.PageSize,
}).Debug("Fetching projects with user-defined pagination")
projects, err := listFunc(opts)
if err != nil {
return nil, err
}
allProjects = projects.Payload
}
return allProjects, nil
}

View File

@ -14,6 +14,8 @@
package project
import (
"fmt"
proj "github.com/goharbor/go-client/pkg/sdk/v2.0/client/project"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
@ -32,30 +34,49 @@ func LogsProjectCommmand() *cobra.Command {
Use: "logs",
Short: "get project logs",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
log.Debug("Starting execution of 'logs' command")
var err error
var resp *proj.GetLogsOK
var projectName string
if len(args) > 0 {
resp, err = api.LogsProject(args[0])
projectName = args[0]
log.Debugf("Project name provided as argument: %s", projectName)
} else {
projectName := prompt.GetProjectNameFromUser()
resp, err = api.LogsProject(projectName)
}
if err != nil {
log.Fatalf("failed to get project logs: %v", err)
return
}
FormatFlag := viper.GetString("output-format")
if FormatFlag != "" {
err = utils.PrintFormat(resp, FormatFlag)
log.Debug("No project name argument provided, prompting user...")
projectName, err = prompt.GetProjectNameFromUser()
if err != nil {
log.Error(err)
return fmt.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
}
log.Debugf("Project name received from prompt: %s", projectName)
}
log.Debugf("Checking if project '%s' exists...", projectName)
projectExists, err := api.CheckProject(projectName)
if err != nil {
return fmt.Errorf("failed to find project: %v ", utils.ParseHarborErrorMsg(err))
} else if !projectExists {
return fmt.Errorf("project %s does not exist", projectName)
}
log.Debugf("Fetching logs for project: %s", projectName)
resp, err = api.LogsProject(projectName)
if err != nil {
return fmt.Errorf("failed to get project logs: %v", utils.ParseHarborErrorMsg(err))
}
formatFlag := viper.GetString("output-format")
if formatFlag != "" {
log.WithField("output_format", formatFlag).Debug("Output format selected")
err = utils.PrintFormat(resp, formatFlag)
if err != nil {
return err
}
} else {
log.Debug("Listing project logs using default view")
auditLog.LogsProject(resp.Payload)
}
return nil
},
}

View File

@ -0,0 +1,37 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package project
import (
"github.com/goharbor/harbor-cli/cmd/harbor/root/project/robot"
"github.com/spf13/cobra"
)
func Robot() *cobra.Command {
cmd := &cobra.Command{
Use: "robot",
Short: "Manage robot accounts",
Example: ` harbor project robot list`,
}
cmd.AddCommand(
robot.ListRobotCommand(),
robot.DeleteRobotCommand(),
robot.ViewRobotCommand(),
robot.CreateRobotCommand(),
robot.UpdateRobotCommand(),
robot.RefreshSecretCommand(),
)
return cmd
}

View File

@ -0,0 +1,251 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package robot
import (
"encoding/json"
"fmt"
"os"
"github.com/atotto/clipboard"
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
"github.com/goharbor/harbor-cli/pkg/api"
config "github.com/goharbor/harbor-cli/pkg/config/robot"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/robot/create"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func CreateRobotCommand() *cobra.Command {
var (
opts create.CreateView
all bool
exportToFile bool
configFile string
)
cmd := &cobra.Command{
Use: "create",
Short: "create robot",
Long: `Create a new robot account within a Harbor project.
Robot accounts are non-human users that can be used for automation purposes
such as CI/CD pipelines, scripts, or other automated processes that need
to interact with Harbor. They have specific permissions and a defined lifetime.
This command supports both interactive and non-interactive modes:
- Without flags: opens an interactive form for configuring the robot
- With flags: creates a robot with the specified parameters
- With config file: loads robot configuration from YAML or JSON
A robot account requires:
- A unique name
- A project where it will be created
- A set of permissions
- A duration (lifetime in days)
The generated robot credentials can be:
- Displayed on screen
- Copied to clipboard (default)
- Exported to a JSON file with the -e flag
Configuration File Format (YAML or JSON):
name: "robot-name" # Required: Name of the robot account
description: "..." # Optional: Description of the robot account
duration: 90 # Required: Lifetime in days
project: "project-name" # Required: Project where the robot will be created
permissions: # Required: At least one permission must be specified
- resource: "repository" # Either specify a single resource
actions: ["pull", "push"]
- resources: ["artifact", "scan"] # Or specify multiple resources
actions: ["read"]
- resource: "project" # Use "*" as an action to grant all available actions
actions: ["*"]
Examples:
# Interactive mode
harbor-cli project robot create
# Non-interactive mode with all flags
harbor-cli project robot create --project myproject --name ci-robot --description "CI pipeline" --duration 90
# Create with all permissions
harbor-cli project robot create --project myproject --name ci-robot --all-permission
# Load from configuration file
harbor-cli project robot create --robot-config-file ./robot-config.yaml
# Export secret to file
harbor-cli project robot create --project myproject --name ci-robot --export-to-file`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
var err error
var permissions []models.Permission
if configFile != "" {
fmt.Println("Loading configuration from: ", configFile)
loadedOpts, loadErr := config.LoadRobotConfigFromFile(configFile)
if loadErr != nil {
return fmt.Errorf("failed to load robot config from file: %v", loadErr)
}
logrus.Info("Successfully loaded robot configuration")
opts = *loadedOpts
if opts.ProjectName == "" {
opts.ProjectName = opts.Permissions[0].Namespace
}
permissions = make([]models.Permission, len(opts.Permissions[0].Access))
for i, access := range opts.Permissions[0].Access {
permissions[i] = models.Permission{
Resource: access.Resource,
Action: access.Action,
}
}
}
if opts.ProjectName == "" && configFile == "" {
opts.ProjectName, err = prompt.GetProjectNameFromUser()
if err != nil {
return fmt.Errorf("%v", utils.ParseHarborErrorMsg(err))
}
if opts.ProjectName == "" {
return fmt.Errorf("project name cannot be empty")
}
}
if len(args) == 0 {
if (opts.Name == "" || opts.Duration == 0) && configFile == "" {
fmt.Println("Opening interactive form for robot creation")
create.CreateRobotView(&opts)
}
if opts.Duration == 0 {
msg := fmt.Errorf("duration cannot be 0")
return fmt.Errorf("failed to create robot: %v", utils.ParseHarborErrorMsg(msg))
}
if len(permissions) == 0 {
if all {
perms, _ := api.GetPermissions()
permission := perms.Payload.Project
choices := []models.Permission{}
for _, perm := range permission {
choices = append(choices, *perm)
}
permissions = choices
} else {
permissions = prompt.GetRobotPermissionsFromUser("project")
if len(permissions) == 0 {
msg := fmt.Errorf("no permissions selected, robot account needs at least one permission")
return fmt.Errorf("failed to create robot: %v", utils.ParseHarborErrorMsg(msg))
}
}
}
// []Permission to []*Access
var accesses []*models.Access
for _, perm := range permissions {
access := &models.Access{
Action: perm.Action,
Resource: perm.Resource,
}
accesses = append(accesses, access)
}
// convert []models.permission to []*model.Access
perm := &create.RobotPermission{
Namespace: opts.ProjectName,
Access: accesses,
Kind: "project", // Default to project level
}
opts.Permissions = []*create.RobotPermission{perm}
}
getProjectID, err := api.GetProject(opts.ProjectName, false)
if err != nil {
return fmt.Errorf("failed to get project: %v", utils.ParseHarborErrorMsg(err))
}
exists, err := api.CheckRoboWithNameExists(getProjectID.Payload.ProjectID, opts.Name)
if err != nil {
return fmt.Errorf("failed to get robot by name: %v", utils.ParseHarborErrorMsg(err))
}
if exists {
return fmt.Errorf("robot account with name '%s' already exists in project '%s'", opts.Name, opts.ProjectName)
}
opts.Level = "project" // Default to project level
response, err := api.CreateRobot(opts)
if err != nil {
return fmt.Errorf("failed to create robot: %v", utils.ParseHarborErrorMsg(err))
}
logrus.Infof("Successfully created robot account '%s' (ID: %d)",
response.Payload.Name, response.Payload.ID)
FormatFlag := viper.GetString("output-format")
if FormatFlag != "" {
name := response.Payload.Name
res, _ := api.GetRobot(response.Payload.ID)
utils.SavePayloadJSON(name, res.Payload)
return nil
}
name, secret := response.Payload.Name, response.Payload.Secret
if exportToFile {
logrus.Info("Exporting robot credentials to file")
exportSecretToFile(name, secret, response.Payload.CreationTime.String(), response.Payload.ExpiresAt)
return nil
} else {
create.CreateRobotSecretView(name, secret)
err = clipboard.WriteAll(response.Payload.Secret)
if err != nil {
logrus.Errorf("failed to write to clipboard")
return nil
}
fmt.Println("secret copied to clipboard.")
return nil
}
},
}
flags := cmd.Flags()
flags.BoolVarP(&all, "all-permission", "a", false, "Select all permissions for the robot account")
flags.BoolVarP(&exportToFile, "export-to-file", "e", false, "Choose to export robot account to file")
flags.StringVarP(&opts.ProjectName, "project", "", "", "set project name")
flags.StringVarP(&opts.Name, "name", "", "", "name of the robot account")
flags.StringVarP(&opts.Description, "description", "", "", "description of the robot account")
flags.Int64VarP(&opts.Duration, "duration", "", 0, "set expiration of robot account in days")
flags.StringVarP(&configFile, "robot-config-file", "r", "", "YAML/JSON file with robot configuration")
return cmd
}
func exportSecretToFile(name, secret, creationTime string, expiresAt int64) {
secretJson := config.RobotSecret{
Name: name,
ExpiresAt: expiresAt,
CreationTime: creationTime,
Secret: secret,
}
filename := fmt.Sprintf("%s-secret.json", name)
jsonData, err := json.MarshalIndent(secretJson, "", " ")
if err != nil {
logrus.Errorf("Failed to marshal secret to JSON: %v", err)
} else {
if err := os.WriteFile(filename, jsonData, 0600); err != nil {
logrus.Errorf("Failed to write secret to file: %v", err)
} else {
fmt.Printf("Secret saved to %s\n", filename)
}
}
}

View File

@ -0,0 +1,93 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package robot
import (
"fmt"
"strconv"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// to-do improve DeleteRobotCommand and multi select & delete
func DeleteRobotCommand() *cobra.Command {
var ProjectName string
cmd := &cobra.Command{
Use: "delete [robotID]",
Short: "delete robot by id",
Long: `Delete a robot account from a Harbor project.
This command permanently removes a robot account from Harbor. Once deleted,
the robot's credentials will no longer be valid, and any automated processes
using those credentials will fail.
The command supports multiple ways to identify the robot account to delete:
- By providing the robot ID directly as an argument
- By specifying a project with the --project flag and selecting the robot interactively
- Without any arguments, which will prompt for both project and robot selection
Important considerations:
- Deletion is permanent and cannot be undone
- All access tokens for the robot will be invalidated immediately
- Any systems using the robot's credentials will need to be updated
Examples:
# Delete robot by ID
harbor-cli project robot delete 123
# Delete robot by selecting from a specific project
harbor-cli project robot delete --project myproject
# Interactive deletion (will prompt for project and robot selection)
harbor-cli project robot delete`,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
var (
robotID int64
err error
)
if len(args) == 1 {
robotID, err = strconv.ParseInt(args[0], 10, 64)
if err != nil {
log.Fatalf("failed to parse robot ID: %v", utils.ParseHarborErrorMsg(err))
}
} else if ProjectName != "" {
project, err := api.GetProject(ProjectName, false)
if err != nil {
log.Fatalf("failed to get project by name %s: %v", ProjectName, utils.ParseHarborErrorMsg(err))
}
robotID = prompt.GetRobotIDFromUser(int64(project.Payload.ProjectID))
} else {
projectID := prompt.GetProjectIDFromUser()
robotID = prompt.GetRobotIDFromUser(projectID)
}
err = api.DeleteRobot(robotID)
if err != nil {
fmt.Printf("failed to delete robots: %v", utils.ParseHarborErrorMsg(err))
return
}
log.Infof("Successfully deleted robot with ID: %d", robotID)
fmt.Printf("Robot account (ID: %d) was successfully deleted\n", robotID)
},
}
flags := cmd.Flags()
flags.StringVarP(&ProjectName, "project", "", "", "set project name")
return cmd
}

View File

@ -0,0 +1,121 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package robot
import (
"strconv"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/constants"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/robot/list"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// ListRobotCommand creates a new `harbor project robot list` command
func ListRobotCommand() *cobra.Command {
var opts api.ListFlags
projectQString := constants.ProjectQString
cmd := &cobra.Command{
Use: "list [projectName]",
Short: "list robot",
Long: `List robot accounts in a Harbor project.
This command displays a list of robot accounts, either from a specific project
or by prompting you to select a project interactively. The list includes basic
information about each robot account, such as ID, name, creation time, and
expiration status.
The command supports multiple ways to specify the project:
- By providing a project name as an argument
- By using the --project-id flag
- By using the -q/--query flag with a project filter
- Without any arguments, which will prompt for project selection
You can control the output using pagination flags and format options:
- Use --page and --page-size to navigate through results
- Use --sort to order the results
- Set output-format in your configuration for JSON, YAML, or other formats
Examples:
# List robots in a specific project by name
harbor-cli project robot list myproject
# List robots in a project by ID
harbor-cli project robot list --project-id 123
# List robots with pagination
harbor-cli project robot list --page 2 --page-size 20
# List robots with custom sorting
harbor-cli project robot list --sort name
# Interactive listing (will prompt for project selection)
harbor-cli project robot list`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
project, err := api.GetProject(args[0], false)
if err != nil {
log.Errorf("Invalid Project Name: %v", err)
}
opts.ProjectID = int64(project.Payload.ProjectID)
opts.Q = projectQString + strconv.FormatInt(opts.ProjectID, 10)
} else if opts.Q != "" {
opts.Q = projectQString + opts.Q
} else if opts.ProjectID > 0 {
opts.Q = projectQString + strconv.FormatInt(opts.ProjectID, 10)
} else {
projectID := prompt.GetProjectIDFromUser()
opts.Q = projectQString + strconv.FormatInt(projectID, 10)
}
robots, err := api.ListRobot(opts)
if err != nil {
log.Errorf("failed to get robots list: %v", err)
}
formatFlag := viper.GetString("output-format")
if formatFlag != "" {
log.WithField("output_format", formatFlag).Debug("Output format selected")
err = utils.PrintFormat(robots, formatFlag)
if err != nil {
return err
}
} else {
list.ListRobots(robots.Payload)
}
return nil
},
}
flags := cmd.Flags()
flags.Int64VarP(&opts.Page, "page", "", 1, "Page number")
flags.Int64VarP(&opts.PageSize, "page-size", "", 10, "Size of per page")
flags.Int64VarP(&opts.ProjectID, "project-id", "", 0, "Project ID")
flags.StringVarP(&opts.Q, "query", "q", "", "Query string to query resources")
flags.StringVarP(
&opts.Sort,
"sort",
"",
"",
"Sort the resource list in ascending or descending order",
)
return cmd
}

View File

@ -0,0 +1,136 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package robot
import (
"fmt"
"strconv"
"github.com/atotto/clipboard"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/robot/create"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func RefreshSecretCommand() *cobra.Command {
var (
robotID int64
secret string
secretStdin bool
)
cmd := &cobra.Command{
Use: "refresh [robotID]",
Short: "refresh robot secret by id",
Long: `Refresh the secret for an existing robot account in Harbor.
This command generates a new secret for a robot account, effectively revoking
the old secret and requiring updates to any systems using the robot's credentials.
The command supports multiple ways to identify the robot account:
- By providing the robot ID directly as an argument
- Without any arguments, which will prompt for both project and robot selection
You can specify the new secret in several ways:
- Let Harbor generate a random secret (default)
- Provide a custom secret with the --secret flag
- Pipe a secret via stdin using the --secret-stdin flag
After refreshing, the new secret will be:
- Displayed on screen
- Copied to clipboard for immediate use
- Usable immediately for authentication
Important considerations:
- The old secret will be invalidated immediately
- Any systems using the old credentials will need to be updated
- There is no way to recover the old secret after refreshing
Examples:
# Refresh robot secret by ID (generates a random secret)
harbor-cli project robot refresh 123
# Refresh with a custom secret
harbor-cli project robot refresh 123 --secret "MyCustomSecret123"
# Provide secret via stdin (useful for scripting)
echo "MySecretFromScript123" | harbor-cli project robot refresh 123 --secret-stdin
# Interactive refresh (will prompt for project and robot selection)
harbor-cli project robot refresh`,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
var err error
if len(args) == 1 {
robotID, err = strconv.ParseInt(args[0], 10, 64)
if err != nil {
log.Fatalf("failed to parse robot ID: %v", err)
}
} else {
projectID := prompt.GetProjectIDFromUser()
robotID = prompt.GetRobotIDFromUser(projectID)
}
if secret != "" {
err = utils.ValidatePassword(secret)
if err != nil {
log.Fatalf("Invalid secret: %v\n", err)
}
}
if secretStdin {
secret = getSecret()
}
response, err := api.RefreshSecret(secret, robotID)
if err != nil {
log.Fatalf("failed to refresh robot secret: %v\n", err)
}
log.Info("Secret updated successfully.")
if response.Payload.Secret != "" {
secret = response.Payload.Secret
create.CreateRobotSecretView("", secret)
err = clipboard.WriteAll(response.Payload.Secret)
if err != nil {
log.Fatalf("failed to write the secret to the clipboard: %v", err)
}
fmt.Println("secret copied to clipboard.")
}
},
}
flags := cmd.Flags()
flags.StringVarP(&secret, "secret", "", "", "secret")
flags.BoolVarP(&secretStdin, "secret-stdin", "", false, "Take the robot secret from stdin")
return cmd
}
// getSecret from commandline
func getSecret() string {
secret, err := utils.GetSecretStdin("Enter your secret: ")
if err != nil {
log.Fatalf("Error reading secret: %v\n", err)
}
if err := utils.ValidatePassword(secret); err != nil {
log.Fatalf("Invalid secret: %v\n", err)
}
return secret
}

View File

@ -0,0 +1,187 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package robot
import (
"strconv"
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/views/robot/update"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func UpdateRobotCommand() *cobra.Command {
var (
robotID int64
opts update.UpdateView
all bool
ProjectName string
)
cmd := &cobra.Command{
Use: "update [robotID]",
Short: "update robot by id",
Long: `Update an existing robot account within a Harbor project.
Robot accounts are non-human users that can be used for automation purposes
such as CI/CD pipelines, scripts, or other automated processes that need
to interact with Harbor. This command allows you to modify an existing robot's
properties including its name, description, duration, and permissions.
This command supports both interactive and non-interactive modes:
- With robot ID: directly updates the specified robot
- With --project flag: helps select a robot from the specified project
- Without either: walks through project and robot selection interactively
The update process will:
1. Identify the robot account to be updated
2. Load its current configuration
3. Apply the requested changes
4. Save the updated configuration
Fields that can be updated:
- Name: The robot account's identifier
- Description: A human-readable description of the robot's purpose
- Duration: The lifetime of the robot account in days
- Permissions: The actions the robot is allowed to perform
Note: Updating a robot does not regenerate its secret. If you need a new
secret, consider deleting the robot and creating a new one instead.
Examples:
# Update robot by ID with a new description
harbor-cli project robot update 123 --description "Updated CI/CD pipeline robot"
# Update robot's duration (extend lifetime)
harbor-cli project robot update 123 --duration 180
# Update by selecting from a specific project
harbor-cli project robot update --project myproject
# Update with all permissions
harbor-cli project robot update 123 --all-permission
# Interactive update (will prompt for robot selection and changes)
harbor-cli project robot update`,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
var err error
if len(args) == 1 {
robotID, err = strconv.ParseInt(args[0], 10, 64)
if err != nil {
log.Fatalf("failed to parse robot ID: %v", err)
}
} else if ProjectName != "" {
project, err := api.GetProject(ProjectName, false)
if err != nil {
log.Fatalf("failed to get project by name %s: %v", ProjectName, err)
}
robotID = prompt.GetRobotIDFromUser(int64(project.Payload.ProjectID))
} else {
projectID := prompt.GetProjectIDFromUser()
robotID = prompt.GetRobotIDFromUser(projectID)
}
robot, err := api.GetRobot(robotID)
if err != nil {
log.Fatalf("failed to get robot: %v", err)
}
bot := robot.Payload
var duration int64
if bot.Duration != nil {
duration = *bot.Duration
}
opts = update.UpdateView{
CreationTime: bot.CreationTime,
Description: bot.Description,
Disable: bot.Disable,
Duration: duration,
Editable: bot.Editable,
ID: bot.ID,
Level: bot.Level,
Name: bot.Name,
Secret: bot.Secret,
}
// declare empty permissions to hold permissions
var permissions []models.Permission
if all {
perms, _ := api.GetPermissions()
permission := perms.Payload.Project
choices := []models.Permission{}
for _, perm := range permission {
choices = append(choices, *perm)
}
permissions = choices
} else {
permissions = prompt.GetRobotPermissionsFromUser("project")
}
// []Permission to []*Access
var accesses []*models.Access
for _, perm := range permissions {
access := &models.Access{
Action: perm.Action,
Resource: perm.Resource,
}
accesses = append(accesses, access)
}
// convert []models.permission to []*model.Access
perm := &update.RobotPermission{
Kind: bot.Permissions[0].Kind,
Namespace: bot.Permissions[0].Namespace,
Access: accesses,
}
opts.Permissions = []*update.RobotPermission{perm}
err = updateRobotView(&opts)
if err != nil {
log.Fatalf("failed to Update robot: %v", err)
}
},
}
flags := cmd.Flags()
flags.BoolVarP(
&all,
"all-permission",
"a",
false,
"Select all permissions for the robot account",
)
flags.StringVarP(&opts.Name, "name", "", "", "name of the robot account")
flags.StringVarP(&opts.Description, "description", "", "", "description of the robot account")
flags.StringVarP(&ProjectName, "project", "", "", "set project name")
flags.Int64VarP(&opts.Duration, "duration", "", 0, "set expiration of robot account in days")
return cmd
}
func updateRobotView(updateView *update.UpdateView) error {
if updateView == nil {
updateView = &update.UpdateView{}
}
update.UpdateRobotView(updateView)
return api.UpdateRobot(updateView)
}

View File

@ -0,0 +1,99 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package robot
import (
"strconv"
"github.com/goharbor/go-client/pkg/sdk/v2.0/client/robot"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/views/robot/view"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func ViewRobotCommand() *cobra.Command {
var (
ProjectName string
)
cmd := &cobra.Command{
Use: "view [robotID]",
Short: "get robot by id",
Long: `View detailed information about a robot account in Harbor.
This command displays comprehensive information about a robot account including
its ID, name, description, creation time, expiration, and the permissions
it has been granted within its project.
The command supports multiple ways to identify the robot account:
- By providing the robot ID directly as an argument
- By specifying a project with the --project flag and selecting the robot interactively
- Without any arguments, which will prompt for both project and robot selection
The displayed information includes:
- Basic details (ID, name, description)
- Temporal information (creation date, expiration date, remaining time)
- Security details (disabled status)
- Detailed permissions breakdown by resource and action
Examples:
# View robot by ID
harbor-cli project robot view 123
# View robot by selecting from a specific project
harbor-cli project robot view --project myproject
# Interactive selection (will prompt for project and robot)
harbor-cli project robot view`,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
var (
robot *robot.GetRobotByIDOK
robotID int64
err error
)
if len(args) == 1 {
robotID, err = strconv.ParseInt(args[0], 10, 64)
if err != nil {
log.Fatalf("failed to parse robot ID: %v", err)
}
} else if ProjectName != "" {
project, err := api.GetProject(ProjectName, false)
if err != nil {
log.Fatalf("failed to get project by name %s: %v", ProjectName, err)
}
robotID = prompt.GetRobotIDFromUser(int64(project.Payload.ProjectID))
} else {
projectID := prompt.GetProjectIDFromUser()
robotID = prompt.GetRobotIDFromUser(projectID)
}
robot, err = api.GetRobot(robotID)
if err != nil {
log.Fatalf("failed to get robot: %v", err)
}
// Convert to a list and display
// robots := &models.Robot{robot.Payload}
view.ViewRobot(robot.Payload)
},
}
flags := cmd.Flags()
flags.StringVarP(&ProjectName, "project", "", "", "set project name")
return cmd
}

View File

@ -14,6 +14,8 @@
package project
import (
"fmt"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/project/list"
@ -27,21 +29,27 @@ func SearchProjectCommand() *cobra.Command {
Use: "search",
Short: "search project based on their names",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
log.Debug("Starting project search command")
projects, err := api.SearchProject(args[0])
if err != nil {
log.Fatalf("failed to get projects: %v", err)
return fmt.Errorf("failed to get projects: %v", utils.ParseHarborErrorMsg(err))
}
log.Debugf("Found %d projects", len(projects.Payload.Project))
if len(projects.Payload.Project) == 0 {
return fmt.Errorf("No projects found with name similar to : %s", args[0])
}
FormatFlag := viper.GetString("output-format")
if FormatFlag != "" {
err = utils.PrintFormat(projects, FormatFlag)
if err != nil {
log.Error(err)
return err
}
} else {
list.SearchProjects(projects.Payload.Project)
}
return nil
},
}
return cmd

View File

@ -14,6 +14,8 @@
package project
import (
"fmt"
"github.com/goharbor/go-client/pkg/sdk/v2.0/client/project"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
@ -30,33 +32,45 @@ func ViewCommand() *cobra.Command {
Use: "view [NAME|ID]",
Short: "get project by name or id",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
var err error
var projectName string
var project *project.GetProjectOK
if len(args) > 0 {
log.Debugf("Project name provided: %s", args[0])
projectName = args[0]
} else {
projectName = prompt.GetProjectNameFromUser()
log.Debug("No project name provided, prompting user")
projectName, err = prompt.GetProjectNameFromUser()
if err != nil {
return fmt.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
}
}
project, err = api.GetProject(projectName)
log.Debugf("Checking existence of project: %s", projectName)
projectExists, err := api.CheckProject(projectName)
if err != nil {
log.Errorf("failed to get project: %v", err)
return
return fmt.Errorf("failed to find project: %v ", utils.ParseHarborErrorMsg(err))
} else if !projectExists {
return fmt.Errorf("project %s does not exist", projectName)
}
log.Debugf("Project %s exists", projectName)
project, err = api.GetProject(projectName, false)
if err != nil {
return fmt.Errorf("failed to get project: %v", utils.ParseHarborErrorMsg(err))
}
FormatFlag := viper.GetString("output-format")
if FormatFlag != "" {
err = utils.PrintFormat(project, FormatFlag)
if err != nil {
log.Error(err)
return
return err
}
} else {
view.ViewProjects(project.Payload)
}
return nil
},
}

View File

@ -0,0 +1,34 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package quota
import (
"github.com/spf13/cobra"
)
func Quota() *cobra.Command {
cmd := &cobra.Command{
Use: "quota",
Short: "Manage quotas",
Long: `Manage quotas of projects`,
Example: ` harbor quota list`,
}
cmd.AddCommand(
ListQuotaCommand(),
ViewQuotaCommand(),
UpdateQuotaCommand(),
)
return cmd
}

View File

@ -0,0 +1,72 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package quota
import (
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/quota/list"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// Lists the Quotas specified for each project
func ListQuotaCommand() *cobra.Command {
var opts api.ListQuotaFlags
cmd := &cobra.Command{
Use: "list",
Short: "list quotas",
Long: "list quotas specified for each project",
Run: func(cmd *cobra.Command, args []string) {
if opts.PageSize > 100 {
log.Errorf("page size should be less than or equal to 100")
return
}
quota, err := api.ListQuota(opts)
if err != nil {
log.Errorf("failed to get quota list: %v", err)
return
}
FormatFlag := viper.GetString("output-format")
if FormatFlag != "" {
err = utils.PrintFormat(quota, FormatFlag)
if err != nil {
log.Errorf("failed to get quota list: %v", err)
return
}
} else {
list.ListQuotas(quota.Payload)
}
},
}
flags := cmd.Flags()
flags.Int64VarP(&opts.Page, "page", "", 1, "Page number")
flags.Int64VarP(&opts.PageSize, "page-size", "", 0, "Size of per page (use 0 to fetch all)")
flags.StringVarP(&opts.Reference, "ref", "", "", "Reference type of quota")
flags.StringVarP(&opts.ReferenceID, "refid", "", "", "Reference ID of quota")
flags.StringVarP(
&opts.Sort,
"sort",
"",
"",
"Sort the resource list in ascending or descending order",
)
return cmd
}

View File

@ -0,0 +1,152 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package quota
import (
"fmt"
"os"
"strconv"
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/quota/update"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
type QuotaUpdateReq struct {
// The new hard limits for the quota
Hard ResourceList `json:"hard,omitempty"`
}
type ResourceList map[string]int64
// UpdateQuotaCommand updates the quota
func UpdateQuotaCommand() *cobra.Command {
var (
storage string
)
var opts api.ListQuotaFlags
cmd := &cobra.Command{
Use: "update [QuotaID]",
Short: "update quotas for projects",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
var err error
var storageValue int64
// get quota id with quota
quota, err := GetQuotaFromUser(args, opts)
if err != nil {
log.Errorf("error: %v", err)
return
}
if storage != "" {
if storage == "-1" {
storageValue = -1
} else {
storageValue, err = utils.StorageStringToBytes(storage)
if err != nil {
log.Errorf("failed to parse storage: %v", err)
os.Exit(1)
}
}
} else {
storage = update.UpdateQuotaView(quota)
storageValue, err = utils.StorageStringToBytes(storage)
if err != nil {
log.Errorf("failed to parse storage: %v", err)
os.Exit(1)
}
}
hardlimit := &models.QuotaUpdateReq{
Hard: models.ResourceList{"storage": storageValue},
}
err = api.UpdateQuota(quota.ID, hardlimit)
if err != nil {
log.Errorf("failed to update quota: %v", err)
os.Exit(1)
}
log.Infof("quota updated successfully!")
},
}
flags := cmd.Flags()
flags.StringVarP(&storage, "storage", "", "", "Enter storage size (e.g., 50GiB, 20MiB, 4TiB)")
flags.StringVarP(&opts.Reference, "project-name", "", "", "Get quota by project-name")
flags.StringVarP(&opts.ReferenceID, "project-id", "", "", "Get quota by project ID")
return cmd
}
func GetQuotaFromUser(args []string, opts api.ListQuotaFlags) (*models.Quota, error) {
var err error
var quota *models.Quota
if len(args) > 0 {
quotaID, err := strconv.ParseInt(args[0], 10, 64)
if err != nil {
err := fmt.Errorf("failed to parse quotaID: %v", err)
return nil, err
}
quota, err = api.GetQuota(int64(quotaID))
if err != nil {
err := fmt.Errorf("failed to get Quota: %v", err)
return nil, err
}
} else if opts.Reference != "" {
project, err := api.GetProject(opts.Reference, false)
if err != nil {
err := fmt.Errorf("failed to get project: %v", err)
return nil, err
}
projectID := project.Payload.ProjectID
quota, err = api.GetQuotaByRef(int64(projectID))
if err != nil {
err := fmt.Errorf("failed to get quota: %v", err)
return nil, err
}
} else if opts.ReferenceID != "" {
projectID, err := strconv.ParseInt(opts.ReferenceID, 10, 64)
if err != nil {
err := fmt.Errorf("invalid projectID: %v", err)
return nil, err
}
quota, err = api.GetQuotaByRef(projectID)
if err != nil {
err := fmt.Errorf("failed to get quota: %v", err)
return nil, err
}
} else {
quotaID := prompt.GetQuotaIDFromUser()
if quotaID == 0 {
err := fmt.Errorf("failed to get quotaID from user")
return nil, err
}
quota, err = api.GetQuota(quotaID)
if err != nil {
err := fmt.Errorf("failed to get quota: %v", err)
return nil, err
}
}
return quota, nil
}

View File

@ -0,0 +1,61 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package quota
import (
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/quota/list"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// View a specified quota
func ViewQuotaCommand() *cobra.Command {
var opts api.ListQuotaFlags
cmd := &cobra.Command{
Use: "view [quotaID]",
Short: "get quota by quota ID",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
var err error
var quota *models.Quota
// get quota id with quota
quota, err = GetQuotaFromUser(args, opts)
if err != nil {
log.Errorf("error: %v", err)
return
}
quotas := []*models.Quota{quota}
FormatFlag := viper.GetString("output-format")
if FormatFlag != "" {
err = utils.PrintFormat(quota, FormatFlag)
if err != nil {
log.Errorf("failed to get quota list: %v", err)
return
}
} else {
list.ListQuotas(quotas)
}
},
}
flags := cmd.Flags()
flags.StringVarP(&opts.Reference, "project-name", "", "", "Get quota by project-name")
flags.StringVarP(&opts.ReferenceID, "project-id", "", "", "Get quota by project ID")
return cmd
}

View File

@ -27,7 +27,7 @@ func CreateRegistryCommand() *cobra.Command {
Use: "create",
Short: "create registry",
Example: "harbor registry create",
Args: cobra.NoArgs,
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
var err error
createView := &api.CreateRegView{

View File

@ -14,6 +14,8 @@
package registry
import (
"sync"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
log "github.com/sirupsen/logrus"
@ -23,21 +25,49 @@ import (
func DeleteRegistryCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "delete",
Short: "delete registry",
Short: "delete registry by name or id",
Example: "harbor registry delete [registryname]",
Args: cobra.MaximumNArgs(1),
Args: cobra.MinimumNArgs(0),
Run: func(cmd *cobra.Command, args []string) {
var err error
var wg sync.WaitGroup
errChan := make(chan error, len(args))
if len(args) > 0 {
registryName, _ := api.GetRegistryIdByName(args[0])
err = api.DeleteRegistry(registryName)
for _, arg := range args {
registryID, _ := api.GetRegistryIdByName(arg)
wg.Add(1)
go func(registryID int64) {
defer wg.Done()
if err := api.DeleteRegistry(registryID); err != nil {
errChan <- err
}
}(registryID)
}
} else {
registryId := prompt.GetRegistryNameFromUser()
err = api.DeleteRegistry(registryId)
err := api.DeleteRegistry(registryId)
if err != nil {
log.Errorf("failed to delete registry: %v", err)
}
}
if err != nil {
log.Errorf("failed to delete registry: %v", err)
// Wait for all goroutines to finish
go func() {
wg.Wait()
close(errChan)
}()
// Collect and handle errors
var finalErr error
for err := range errChan {
if finalErr == nil {
finalErr = err
} else {
log.Errorf("Error: %v", err)
}
}
if finalErr != nil {
log.Errorf("failed to delete registry: %v", finalErr)
}
},
}

View File

@ -14,6 +14,8 @@
package registry
import (
"fmt"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/registry/list"
@ -29,22 +31,30 @@ func ListRegistryCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "list registry",
Run: func(cmd *cobra.Command, args []string) {
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
if opts.PageSize > 100 {
return fmt.Errorf("page size should be less than or equal to 100")
}
registry, err := api.ListRegistries(opts)
if err != nil {
log.Fatalf("failed to get projects list: %v", err)
return
return fmt.Errorf("failed to get projects list: %v", err)
}
FormatFlag := viper.GetString("output-format")
if FormatFlag != "" {
err = utils.PrintFormat(registry, FormatFlag)
if len(registry.Payload) == 0 {
log.Info("No registries found")
return nil
}
formatFlag := viper.GetString("output-format")
if formatFlag != "" {
err = utils.PrintFormat(registry, formatFlag)
if err != nil {
log.Error(err)
}
} else {
list.ListRegistry(registry.Payload)
}
return nil
},
}

View File

@ -45,7 +45,11 @@ func UpdateRegistryCommand() *cobra.Command {
registryId = prompt.GetRegistryNameFromUser()
}
existingRegistry := api.GetRegistryResponse(registryId)
existingRegistry, err := api.GetRegistryResponse(registryId)
if err != nil {
log.Errorf("failed to get registry with ID %d: %v", registryId, err)
return
}
if existingRegistry == nil {
log.Errorf("registry is not found")
return

View File

@ -22,10 +22,14 @@ func Replication() *cobra.Command {
var replicationCmd = &cobra.Command{
Use: "replication",
Aliases: []string{"repl"},
Short: "",
Long: ``,
Short: "Manage replications",
Long: `Manage replications in Harbor context`,
}
replicationCmd.AddCommand()
replicationCmd.AddCommand(
ReplicationPoliciesCommand(),
StartCommand(),
StopCommand(),
)
return replicationCmd
}

View File

@ -0,0 +1,38 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package replication
import (
rpolicies "github.com/goharbor/harbor-cli/cmd/harbor/root/replication/policies"
"github.com/spf13/cobra"
)
func ReplicationPoliciesCommand() *cobra.Command {
// replicationCmd represents the replication command.
var replicationCmd = &cobra.Command{
Use: "policies",
Aliases: []string{"pol"},
Short: "Manage replication policies",
Long: `Manage replication policies in Harbor context`,
}
replicationCmd.AddCommand(
rpolicies.ListCommand(),
rpolicies.ViewCommand(),
rpolicies.DeleteCommand(),
rpolicies.CreateCommand(),
rpolicies.UpdateCommand(),
)
return replicationCmd
}

View File

@ -0,0 +1,185 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package policies
import (
"fmt"
"strconv"
"strings"
"github.com/goharbor/go-client/pkg/sdk/v2.0/client/replication"
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
"github.com/goharbor/harbor-cli/pkg/api"
config "github.com/goharbor/harbor-cli/pkg/config/replication"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/replication/policies/create"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func CreateCommand() *cobra.Command {
var configFile string
var registryID int64
var err error
var opts *create.CreateView
cmd := &cobra.Command{
Use: "create",
Short: "create replication policies",
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
log.Debug("Starting replications create command")
if configFile != "" {
log.Debugf("Loading replication policy configuration from file: %s", configFile)
opts, err = config.LoadConfigFromFile(configFile)
if err != nil {
return fmt.Errorf("failed to load replication policy configuration: %v", err)
}
registryID, err = api.GetRegistryIdByName(opts.TargetRegistry)
if err != nil {
return fmt.Errorf("failed to get registry ID for name %s: %v", opts.TargetRegistry, err)
}
if registryID == 0 {
return fmt.Errorf("registry with name %s not found", opts.TargetRegistry)
}
} else {
opts = &create.CreateView{}
create.CreateRPolicyView(opts, false)
registryID = prompt.GetRegistryNameFromUser()
}
registry, err := api.GetRegistryResponse(registryID)
if err != nil {
return fmt.Errorf("failed to get registry with ID %d: %v", registryID, err)
}
policy := ConvertToPolicy(opts, registry)
response, err := api.CreateReplicationPolicy(&replication.CreateReplicationPolicyParams{
Policy: policy,
})
if err != nil {
return fmt.Errorf("failed to create replication policy: %v", utils.ParseHarborErrorMsg(err))
}
fmt.Println("Replication policy created successfully with ID:", response.Location)
return nil
},
}
flags := cmd.Flags()
flags.StringVarP(&configFile, "policy-config-file", "f", "", "YAML/JSON file with robot configuration")
return cmd
}
func ConvertToPolicy(view *create.CreateView, registry *models.Registry) *models.ReplicationPolicy {
policy := &models.ReplicationPolicy{
Name: view.Name,
Description: view.Description,
Enabled: view.Enabled,
Override: view.Override,
// ReplicateDeletion is the favored field to use for deletion replication
// Deletion is deprecated and will be removed in future versions
// However, for updating from false to true, we need to set both fields
ReplicateDeletion: view.ReplicateDeletion,
Deletion: view.ReplicateDeletion,
CopyByChunk: &view.CopyByChunk,
Filters: []*models.ReplicationFilter{},
}
if view.Speed != "" {
speedInt, _ := strconv.ParseInt(view.Speed, 10, 32)
speed := int32(speedInt)
policy.Speed = &speed
}
trigger := &models.ReplicationTrigger{
Type: view.TriggerType,
}
if view.TriggerType == "scheduled" {
trigger.TriggerSettings = &models.ReplicationTriggerSettings{
Cron: view.CronString,
}
}
policy.Trigger = trigger
if view.ReplicationMode == "Pull" {
// Pull mode (external -> Harbor)
policy.SrcRegistry = registry
policy.DestRegistry = nil
} else {
// Push mode (Harbor -> external)
policy.SrcRegistry = nil
policy.DestRegistry = registry
}
var resourceFilter *models.ReplicationFilter
var nameFilter *models.ReplicationFilter
var tagFilter *models.ReplicationFilter
// var labelFilter *models.ReplicationFilter
var filters []*models.ReplicationFilter
if view.ResourceFilter != "" {
resourceFilter = &models.ReplicationFilter{
Type: "resource",
Value: view.ResourceFilter,
Decoration: "",
}
filters = append(filters, resourceFilter)
}
if view.NameFilter != "" {
nameFilter = &models.ReplicationFilter{
Type: "name",
Value: view.NameFilter,
Decoration: "",
}
filters = append(filters, nameFilter)
}
if view.TagPattern != "" {
tagFilter = &models.ReplicationFilter{
Type: "tag",
Value: view.TagPattern,
Decoration: view.TagFilter,
}
filters = append(filters, tagFilter)
}
if view.LabelPattern != "" {
decoration := "matches"
if view.LabelFilter == "excludes" {
decoration = "excludes"
}
var labelValues []string
if strings.Contains(view.LabelPattern, ",") {
labelValues = strings.Split(view.LabelPattern, ",")
for i, label := range labelValues {
labelValues[i] = strings.TrimSpace(label)
}
} else {
labelValues = []string{strings.TrimSpace(view.LabelPattern)}
}
filters = append(filters, &models.ReplicationFilter{
Type: "label",
Value: labelValues,
Decoration: decoration,
})
}
policy.Filters = filters
return policy
}

View File

@ -0,0 +1,56 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package policies
import (
"fmt"
"strconv"
"github.com/spf13/cobra"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
)
func DeleteCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "delete [NAME|ID]",
Short: "delete replication policy by name or id",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var rpolicyID int64
if len(args) > 0 {
var err error
// convert string to int64
rpolicyID, err = strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("invalid replication policy ID: %s, %v", args[0], err)
}
} else {
rpolicyID = prompt.GetReplicationPolicyFromUser()
}
_, err := api.DeleteReplicationPolicy(rpolicyID)
if err != nil {
return fmt.Errorf("failed to get replication policy: %v", utils.ParseHarborErrorMsg(err))
}
fmt.Printf("Replication policy %d deleted successfully\n", rpolicyID)
return nil
},
}
return cmd
}

View File

@ -0,0 +1,74 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package policies
import (
"fmt"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/replication/policies/list"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func ListCommand() *cobra.Command {
var opts api.ListFlags
cmd := &cobra.Command{
Use: "list",
Short: "List replication policies",
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
log.Debug("Starting replications list command")
if opts.PageSize > 100 {
return fmt.Errorf("page size should be less than or equal to 100")
}
log.Debug("Fetching projects...")
allPolicies, err := api.ListReplicationPolicies(opts)
if err != nil {
return fmt.Errorf("failed to get projects list: %v", utils.ParseHarborErrorMsg(err))
}
log.WithField("count", len(allPolicies.Payload)).Debug("Number of projects fetched")
if len(allPolicies.Payload) == 0 {
log.Info("No policies found")
return nil
}
formatFlag := viper.GetString("output-format")
if formatFlag != "" {
log.WithField("output_format", formatFlag).Debug("Output format selected")
err = utils.PrintFormat(allPolicies.Payload, formatFlag)
if err != nil {
return err
}
} else {
log.Debug("Listing projects using default view")
list.ListPolicies(allPolicies.Payload)
}
return nil
},
}
flags := cmd.Flags()
flags.Int64VarP(&opts.Page, "page", "", 1, "Page number")
flags.Int64VarP(&opts.PageSize, "page-size", "", 0, "Size of per page (0 to fetch all)")
flags.StringVarP(&opts.Q, "query", "q", "", "Query string to query resources")
flags.StringVarP(&opts.Sort, "sort", "", "", "Sort the resource list in ascending or descending order")
return cmd
}

View File

@ -0,0 +1,123 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package policies
import (
"fmt"
"strconv"
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/views/replication/policies/create"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// UpdateCommand returns a command to update existing replication policies
func UpdateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "update [policy-id]",
Short: "Update an existing replication policy",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var policyID int64
if len(args) > 0 {
var err error
policyID, err = strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("invalid replication policy ID: %s, %v", args[0], err)
}
} else {
policyID = prompt.GetReplicationPolicyFromUser()
}
existingPolicy, err := api.GetReplicationPolicy(policyID)
if err != nil {
return fmt.Errorf("failed to get replication policy: %w", err)
}
var existingReplicationMode string
if existingPolicy.Payload.SrcRegistry.ID != 0 && existingPolicy.Payload.DestRegistry.ID == 0 {
existingReplicationMode = "Pull"
} else if existingPolicy.Payload.SrcRegistry.ID == 0 && existingPolicy.Payload.DestRegistry.ID != 0 {
existingReplicationMode = "Push"
} else {
return fmt.Errorf("replication policy with ID %d is neither Pull nor Push", policyID)
}
createView := &create.CreateView{
Name: existingPolicy.Payload.Name,
Description: existingPolicy.Payload.Description,
Enabled: existingPolicy.Payload.Enabled,
Override: existingPolicy.Payload.Override,
ReplicateDeletion: existingPolicy.Payload.ReplicateDeletion,
ReplicationMode: existingReplicationMode,
}
if existingPolicy.Payload.CopyByChunk != nil {
createView.CopyByChunk = *existingPolicy.Payload.CopyByChunk
}
if existingPolicy.Payload.Speed != nil {
if *existingPolicy.Payload.Speed == 0 {
speed := int32(-1)
existingPolicy.Payload.Speed = &speed
}
createView.Speed = strconv.FormatInt(int64(*existingPolicy.Payload.Speed), 10)
}
if existingPolicy.Payload.SrcRegistry != nil && existingPolicy.Payload.DestRegistry == nil {
createView.ReplicationMode = "Pull"
} else if existingPolicy.Payload.SrcRegistry == nil && existingPolicy.Payload.DestRegistry != nil {
createView.ReplicationMode = "Push"
}
if existingPolicy.Payload.Trigger != nil {
createView.TriggerType = existingPolicy.Payload.Trigger.Type
if existingPolicy.Payload.Trigger.TriggerSettings != nil {
if existingPolicy.Payload.Trigger.Type == "scheduled" {
createView.CronString = existingPolicy.Payload.Trigger.TriggerSettings.Cron
} else if existingPolicy.Payload.Trigger.Type == "event_based" {
createView.ReplicateDeletion = existingPolicy.Payload.ReplicateDeletion
}
}
}
log.Infof("Updating replication policy: %s (ID: %d)", existingPolicy.Payload.Name, policyID)
create.CreateRPolicyView(createView, true)
var updatedPolicy *models.ReplicationPolicy
fmt.Println("Updated policy replicate deletion:", createView.ReplicateDeletion)
if createView.ReplicationMode == "Pull" {
updatedPolicy = ConvertToPolicy(createView, existingPolicy.Payload.SrcRegistry)
updatedPolicy.ID = policyID
} else {
updatedPolicy = ConvertToPolicy(createView, existingPolicy.Payload.DestRegistry)
}
_, err = api.UpdateReplicationPolicy(policyID, updatedPolicy)
if err != nil {
return fmt.Errorf("failed to update replication policy: %w", err)
}
log.Infof("Successfully updated replication policy: %s (ID: %d)", updatedPolicy.Name, policyID)
return nil
},
}
return cmd
}

View File

@ -0,0 +1,66 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package policies
import (
"fmt"
"strconv"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
view "github.com/goharbor/harbor-cli/pkg/views/replication/policies/view"
)
func ViewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "view [NAME|ID]",
Short: "get replication policy by name or id",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var rpolicyID int64
if len(args) > 0 {
var err error
// convert string to int64
rpolicyID, err = strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("invalid replication policy ID: %s, %v", args[0], err)
}
} else {
rpolicyID = prompt.GetReplicationPolicyFromUser()
}
response, err := api.GetReplicationPolicy(rpolicyID)
if err != nil {
return fmt.Errorf("failed to get replication policy: %v", utils.ParseHarborErrorMsg(err))
}
FormatFlag := viper.GetString("output-format")
if FormatFlag != "" {
err = utils.PrintFormat(response.Payload, FormatFlag)
if err != nil {
return err
}
} else {
view.ViewPolicy(response.Payload)
}
return nil
},
}
return cmd
}

View File

@ -0,0 +1,56 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package replication
import (
"fmt"
"strconv"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func StartCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "start",
Short: "start replication",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
log.Debug("Starting replication")
var rpolicyID int64
if len(args) > 0 {
var err error
// convert string to int64
rpolicyID, err = strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("invalid replication policy ID: %s, %v", args[0], err)
}
} else {
rpolicyID = prompt.GetReplicationPolicyFromUser()
}
response, err := api.StartReplication(rpolicyID)
if err != nil {
return fmt.Errorf("failed to start replication: %v", utils.ParseHarborErrorMsg(err))
}
fmt.Printf("Repliation started successfully with ID: %s\n", response.Location)
return nil
},
}
return cmd
}

View File

@ -0,0 +1,68 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package replication
import (
"fmt"
"strconv"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func StopCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "stop",
Short: "stop replication",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
log.Debug("Stopping replication")
var rpolicyID int64
var executionID int64
if len(args) > 0 {
var err error
// convert string to int64
rpolicyID, err = strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("invalid replication policy ID: %s, %v", args[0], err)
}
executionID = prompt.GetReplicationExecutionIDFromUser(rpolicyID)
} else {
rpolicyID = prompt.GetReplicationPolicyFromUser()
executionID = prompt.GetReplicationExecutionIDFromUser(rpolicyID)
}
execution, err := api.GetReplicationExecution(executionID)
if err != nil {
return fmt.Errorf("failed to get replication execution: %v", utils.ParseHarborErrorMsg(err))
}
if execution.Payload.Status != "InProgress" {
return fmt.Errorf("replication execution with ID: %d is already stopped, succeed or failed", executionID)
}
_, err = api.StopReplication(executionID)
if err != nil {
return fmt.Errorf("failed to stop replication: %v", utils.ParseHarborErrorMsg(err))
}
fmt.Printf("Replication execution with ID: %d stopped successfully\n", executionID)
return nil
},
}
return cmd
}

View File

@ -29,14 +29,22 @@ func RepoDeleteCmd() *cobra.Command {
Long: `Delete a repository within a project in Harbor`,
Run: func(cmd *cobra.Command, args []string) {
var err error
var projectName string
var repoName string
if len(args) > 0 {
projectName, repoName := utils.ParseProjectRepo(args[0])
err = api.RepoDelete(projectName, repoName)
projectName, repoName, err = utils.ParseProjectRepo(args[0])
if err != nil {
log.Errorf("failed to parse project/repo: %v", err)
return
}
} else {
projectName := prompt.GetProjectNameFromUser()
repoName := prompt.GetRepoNameFromUser(projectName)
err = api.RepoDelete(projectName, repoName)
projectName, err = prompt.GetProjectNameFromUser()
if err != nil {
log.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
}
repoName = prompt.GetRepoNameFromUser(projectName)
}
err = api.RepoDelete(projectName, repoName, false)
if err != nil {
log.Errorf("failed to delete repository: %v", err)
}

View File

@ -14,6 +14,8 @@
package repository
import (
"fmt"
"github.com/goharbor/go-client/pkg/sdk/v2.0/client/repository"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
@ -33,7 +35,10 @@ func ListRepositoryCommand() *cobra.Command {
Example: ` harbor repo list <project_name>`,
Long: `Get information of all repositories in a project`,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
if opts.PageSize > 100 {
return fmt.Errorf("page size should be less than or equal to 100")
}
var err error
var repos repository.ListRepositoriesOK
var projectName string
@ -41,15 +46,20 @@ func ListRepositoryCommand() *cobra.Command {
if len(args) > 0 {
projectName = args[0]
} else {
projectName = prompt.GetProjectNameFromUser()
projectName, err = prompt.GetProjectNameFromUser()
if err != nil {
return fmt.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
}
}
repos, err = api.ListRepository(projectName)
repos, err = api.ListRepository(projectName, false, opts)
if err != nil {
log.Errorf("failed to list repositories: %v", err)
return
return fmt.Errorf("failed to list repositories: %v", err)
}
if len(repos.Payload) == 0 {
log.Info("No repositories found")
return nil
}
FormatFlag := viper.GetString("output-format")
if FormatFlag != "" {
err = utils.PrintFormat(repos, FormatFlag)
@ -59,7 +69,7 @@ func ListRepositoryCommand() *cobra.Command {
} else {
list.ListRepositories(repos.Payload)
}
list.ListRepositories(repos.Payload)
return nil
},
}

View File

@ -36,14 +36,20 @@ func RepoViewCmd() *cobra.Command {
var repo *repository.GetRepositoryOK
if len(args) > 0 {
projectName, repoName = utils.ParseProjectRepo(args[0])
projectName, repoName, err = utils.ParseProjectRepo(args[0])
if err != nil {
log.Errorf("failed to parse project/repo: %v", err)
return
}
} else {
projectName = prompt.GetProjectNameFromUser()
projectName, err = prompt.GetProjectNameFromUser()
if err != nil {
log.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
}
repoName = prompt.GetRepoNameFromUser(projectName)
}
repo, err = api.RepoView(projectName, repoName)
if err != nil {
log.Errorf("failed to get repository information: %v", err)
return

View File

@ -0,0 +1,36 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package robot
import (
"github.com/spf13/cobra"
)
func Robot() *cobra.Command {
cmd := &cobra.Command{
Use: "robot",
Short: "Manage robot accounts",
Example: ` harbor robot list`,
}
cmd.AddCommand(
ListRobotCommand(),
DeleteRobotCommand(),
ViewRobotCommand(),
CreateRobotCommand(),
UpdateRobotCommand(),
RefreshSecretCommand(),
)
return cmd
}

View File

@ -0,0 +1,439 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package robot
import (
"encoding/json"
"fmt"
"os"
"github.com/atotto/clipboard"
"github.com/charmbracelet/huh"
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
"github.com/goharbor/harbor-cli/pkg/api"
config "github.com/goharbor/harbor-cli/pkg/config/robot"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/robot/create"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func CreateRobotCommand() *cobra.Command {
var (
opts create.CreateView
all bool
exportToFile bool
configFile string
)
cmd := &cobra.Command{
Use: "create",
Short: "create robot",
Long: `Create a new robot account within Harbor.
Robot accounts are non-human users that can be used for automation purposes
such as CI/CD pipelines, scripts, or other automated processes that need
to interact with Harbor. They have specific permissions and a defined lifetime.
This command creates system-level robots that can have permissions spanning
multiple projects, making them suitable for automation tasks that need access
across your Harbor instance.
This command supports both interactive and non-interactive modes:
- Without flags: opens an interactive form for configuring the robot
- With flags: creates a robot with the specified parameters
- With config file: loads robot configuration from YAML or JSON
A robot account requires:
- A unique name
- A set of system permissions
- Optional project-specific permissions
- A duration (lifetime in days)
The generated robot credentials can be:
- Displayed on screen
- Copied to clipboard (default)
- Exported to a JSON file with the -e flag
Examples:
# Interactive mode
harbor-cli robot create
# Non-interactive mode with all flags
harbor-cli robot create --name ci-robot --description "CI pipeline" --duration 90
# Create with all permissions
harbor-cli robot create --name ci-robot --all-permission
# Load from configuration file
harbor-cli robot create --robot-config-file ./robot-config.yaml
# Export secret to file
harbor-cli robot create --name ci-robot --export-to-file`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
var permissions []models.Permission
var projectPermissionsMap = make(map[string][]models.Permission)
var accessesSystem []*models.Access
// Handle config file or interactive input
if configFile != "" {
if err := loadFromConfigFile(&opts, configFile, &permissions, projectPermissionsMap); err != nil {
return err
}
} else {
if err := handleInteractiveInput(&opts, all, &permissions, projectPermissionsMap); err != nil {
return err
}
}
// Build system access permissions
for _, perm := range permissions {
accessesSystem = append(accessesSystem, &models.Access{
Resource: perm.Resource,
Action: perm.Action,
})
}
// Build merged permissions structure
opts.Permissions = buildMergedPermissions(projectPermissionsMap, accessesSystem)
opts.Level = "system"
// Create robot and handle response
return createRobotAndHandleResponse(&opts, exportToFile)
},
}
addFlags(cmd, &opts, &all, &exportToFile, &configFile)
return cmd
}
func loadFromConfigFile(opts *create.CreateView, configFile string, permissions *[]models.Permission, projectPermissionsMap map[string][]models.Permission) error {
fmt.Println("Loading configuration from: ", configFile)
loadedOpts, err := config.LoadRobotConfigFromFile(configFile)
if err != nil {
return fmt.Errorf("failed to load robot config from file: %v", err)
}
logrus.Info("Successfully loaded robot configuration")
*opts = *loadedOpts
// Extract system-level and project permissions
var systemPermFound bool
for _, perm := range opts.Permissions {
if perm.Kind == "system" && perm.Namespace == "/" {
systemPermFound = true
*permissions = make([]models.Permission, len(perm.Access))
for i, access := range perm.Access {
(*permissions)[i] = models.Permission{
Resource: access.Resource,
Action: access.Action,
}
}
} else if perm.Kind == "project" {
var projectPerms []models.Permission
for _, access := range perm.Access {
projectPerms = append(projectPerms, models.Permission{
Resource: access.Resource,
Action: access.Action,
})
}
projectPermissionsMap[perm.Namespace] = projectPerms
}
}
if !systemPermFound {
return fmt.Errorf("system robot configuration must include system-level permissions")
}
logrus.Infof("Loaded system robot with %d system permissions and %d project-specific permissions",
len(*permissions), len(projectPermissionsMap))
return nil
}
func handleInteractiveInput(opts *create.CreateView, all bool, permissions *[]models.Permission, projectPermissionsMap map[string][]models.Permission) error {
// Show interactive form if needed
if opts.Name == "" || opts.Duration == 0 {
create.CreateRobotView(opts)
}
// Validate duration
if opts.Duration == 0 {
return fmt.Errorf("failed to create robot: %v", utils.ParseHarborErrorMsg(fmt.Errorf("duration cannot be 0")))
}
// Get system permissions
if err := getSystemPermissions(all, permissions); err != nil {
return err
}
// Get project permissions
return getProjectPermissions(opts, projectPermissionsMap)
}
func getSystemPermissions(all bool, permissions *[]models.Permission) error {
if len(*permissions) == 0 {
if all {
perms, _ := api.GetPermissions()
for _, perm := range perms.Payload.System {
*permissions = append(*permissions, *perm)
}
} else {
*permissions = prompt.GetRobotPermissionsFromUser("system")
if len(*permissions) == 0 {
return fmt.Errorf("failed to create robot: %v",
utils.ParseHarborErrorMsg(fmt.Errorf("no permissions selected, robot account needs at least one permission")))
}
}
}
return nil
}
func getProjectPermissions(opts *create.CreateView, projectPermissionsMap map[string][]models.Permission) error {
permissionMode, err := promptPermissionMode()
if err != nil {
return fmt.Errorf("error selecting permission mode: %v", err)
}
switch permissionMode {
case "list":
return handleMultipleProjectsPermissions(projectPermissionsMap)
case "per_project":
return handlePerProjectPermissions(opts, projectPermissionsMap)
case "none":
fmt.Println("Creating robot with system-level permissions only (no project-specific permissions)")
return nil
default:
return fmt.Errorf("unknown permission mode: %s", permissionMode)
}
}
func handleMultipleProjectsPermissions(projectPermissionsMap map[string][]models.Permission) error {
selectedProjects, err := getMultipleProjectsFromUser()
if err != nil {
return fmt.Errorf("error selecting projects: %v", err)
}
if len(selectedProjects) > 0 {
fmt.Println("Select permissions to apply to all selected projects:")
projectPermissions := prompt.GetRobotPermissionsFromUser("project")
for _, projectName := range selectedProjects {
projectPermissionsMap[projectName] = projectPermissions
}
}
return nil
}
func handlePerProjectPermissions(opts *create.CreateView, projectPermissionsMap map[string][]models.Permission) error {
if opts.ProjectName == "" {
for {
projectName, err := prompt.GetProjectNameFromUser()
if err != nil {
return fmt.Errorf("%v", utils.ParseHarborErrorMsg(err))
}
if projectName == "" {
return fmt.Errorf("project name cannot be empty")
}
projectPermissionsMap[projectName] = prompt.GetRobotPermissionsFromUser("project")
moreProjects, err := promptMoreProjects()
if err != nil {
return fmt.Errorf("error asking for more projects: %v", err)
}
if !moreProjects {
break
}
}
} else {
projectPermissions := prompt.GetRobotPermissionsFromUser("project")
projectPermissionsMap[opts.ProjectName] = projectPermissions
}
return nil
}
func buildMergedPermissions(projectPermissionsMap map[string][]models.Permission, accessesSystem []*models.Access) []*create.RobotPermission {
var mergedPermissions []*create.RobotPermission
// Add project permissions
for projectName, projectPermissions := range projectPermissionsMap {
var accessesProject []*models.Access
for _, perm := range projectPermissions {
accessesProject = append(accessesProject, &models.Access{
Resource: perm.Resource,
Action: perm.Action,
})
}
mergedPermissions = append(mergedPermissions, &create.RobotPermission{
Namespace: projectName,
Access: accessesProject,
Kind: "project",
})
}
// Add system permissions
mergedPermissions = append(mergedPermissions, &create.RobotPermission{
Namespace: "/",
Access: accessesSystem,
Kind: "system",
})
return mergedPermissions
}
func createRobotAndHandleResponse(opts *create.CreateView, exportToFile bool) error {
response, err := api.CreateRobot(*opts)
if err != nil {
return fmt.Errorf("failed to create robot: %v", utils.ParseHarborErrorMsg(err))
}
logrus.Infof("Successfully created robot account '%s' (ID: %d)",
response.Payload.Name, response.Payload.ID)
// Handle output format
if formatFlag := viper.GetString("output-format"); formatFlag != "" {
res, _ := api.GetRobot(response.Payload.ID)
utils.SavePayloadJSON(response.Payload.Name, res.Payload)
return nil
}
// Handle secret output
name, secret := response.Payload.Name, response.Payload.Secret
if exportToFile {
logrus.Info("Exporting robot credentials to file")
exportSecretToFile(name, secret, response.Payload.CreationTime.String(), response.Payload.ExpiresAt)
return nil
}
create.CreateRobotSecretView(name, secret)
if err := clipboard.WriteAll(secret); err != nil {
logrus.Errorf("failed to write to clipboard")
} else {
fmt.Println("secret copied to clipboard.")
}
return nil
}
func addFlags(cmd *cobra.Command, opts *create.CreateView, all *bool, exportToFile *bool, configFile *string) {
flags := cmd.Flags()
flags.BoolVarP(all, "all-permission", "a", false, "Select all permissions for the robot account")
flags.BoolVarP(exportToFile, "export-to-file", "e", false, "Choose to export robot account to file")
flags.StringVarP(&opts.ProjectName, "project", "", "", "set project name")
flags.StringVarP(&opts.Name, "name", "", "", "name of the robot account")
flags.StringVarP(&opts.Description, "description", "", "", "description of the robot account")
flags.Int64VarP(&opts.Duration, "duration", "", 0, "set expiration of robot account in days")
flags.StringVarP(configFile, "robot-config-file", "r", "", "YAML/JSON file with robot configuration")
}
func exportSecretToFile(name, secret, creationTime string, expiresAt int64) {
secretJson := config.RobotSecret{
Name: name,
ExpiresAt: expiresAt,
CreationTime: creationTime,
Secret: secret,
}
filename := fmt.Sprintf("%s-secret.json", name)
jsonData, err := json.MarshalIndent(secretJson, "", " ")
if err != nil {
logrus.Errorf("Failed to marshal secret to JSON: %v", err)
return
}
if err := os.WriteFile(filename, jsonData, 0600); err != nil {
logrus.Errorf("Failed to write secret to file: %v", err)
return
}
fmt.Printf("Secret saved to %s\n", filename)
}
func getMultipleProjectsFromUser() ([]string, error) {
allProjects, err := api.ListAllProjects()
if err != nil {
return nil, fmt.Errorf("failed to list projects: %v", err)
}
var selectedProjects []string
var projectOptions []huh.Option[string]
for _, p := range allProjects.Payload {
projectOptions = append(projectOptions, huh.NewOption(p.Name, p.Name))
}
err = huh.NewForm(
huh.NewGroup(
huh.NewNote().
Title("Multiple Project Selection").
Description("Select the projects to assign the same permissions to this robot account."),
huh.NewMultiSelect[string]().
Title("Select projects").
Options(projectOptions...).
Value(&selectedProjects),
),
).WithTheme(huh.ThemeCharm()).WithWidth(80).Run()
return selectedProjects, err
}
func promptMoreProjects() (bool, error) {
var addMore bool
err := huh.NewForm(
huh.NewGroup(
huh.NewNote().
Title("Project Selection").
Description("You can add permissions for multiple projects to this robot account."),
huh.NewSelect[bool]().
Title("Do you want to select (more) projects?").
Description("Select 'Yes' to add (another) project, 'No' to continue with current selection.").
Options(
huh.NewOption("No", false),
huh.NewOption("Yes", true),
).
Value(&addMore),
),
).WithTheme(huh.ThemeCharm()).WithWidth(60).WithHeight(10).Run()
return addMore, err
}
func promptPermissionMode() (string, error) {
var permissionMode string
err := huh.NewForm(
huh.NewGroup(
huh.NewNote().
Title("Permission Mode").
Description("Select how you want to assign permissions to projects:"),
huh.NewSelect[string]().
Title("Permission Mode").
Description("Choose 'List' to select multiple projects with common permissions, or 'Per Project' for individual project permissions.").
Options(
huh.NewOption("No project permissions (system-level only)", "none"),
huh.NewOption("Per Project", "per_project"),
huh.NewOption("List", "list"),
).
Value(&permissionMode),
),
).WithTheme(huh.ThemeCharm()).WithWidth(60).WithHeight(10).Run()
return permissionMode, err
}

View File

@ -0,0 +1,80 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package robot
import (
"fmt"
"strconv"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// to-do improve DeleteRobotCommand and multi select & delete
func DeleteRobotCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "delete [robotID]",
Short: "delete robot by id",
Long: `Delete a robot account from Harbor.
This command permanently removes a robot account from Harbor. Once deleted,
the robot's credentials will no longer be valid, and any automated processes
using those credentials will fail.
The command supports multiple ways to identify the robot account to delete:
- By providing the robot ID directly as an argument
- Without any arguments, which will prompt for robot selection
Important considerations:
- Deletion is permanent and cannot be undone
- All access tokens for the robot will be invalidated immediately
- Any systems using the robot's credentials will need to be updated
- For system robots, access across all projects will be revoked
Examples:
# Delete robot by ID
harbor-cli robot delete 123
# Interactive deletion (will prompt for robot selection)
harbor-cli robot delete`,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
var (
robotID int64
err error
)
if len(args) == 1 {
robotID, err = strconv.ParseInt(args[0], 10, 64)
if err != nil {
log.Fatalf("failed to parse robot ID: %v", err)
}
} else {
robotID = prompt.GetRobotIDFromUser(-1)
}
err = api.DeleteRobot(robotID)
if err != nil {
fmt.Printf("failed to delete robots: %v", utils.ParseHarborErrorMsg(err))
return
}
log.Infof("Successfully deleted robot with ID: %d", robotID)
fmt.Printf("Robot account (ID: %d) was successfully deleted\n", robotID)
},
}
return cmd
}

View File

@ -0,0 +1,95 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package robot
import (
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/robot/list"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// ListRobotCommand creates a new `harbor project robot list` command
func ListRobotCommand() *cobra.Command {
var opts api.ListFlags
cmd := &cobra.Command{
Use: "list [projectName]",
Short: "list robot",
Long: `List robot accounts in Harbor.
This command displays a list of system-level robot accounts. The list includes basic
information about each robot account, such as ID, name, creation time, and
expiration status.
System-level robots have permissions that can span across multiple projects, making
them suitable for CI/CD pipelines and automation tasks that require access to
multiple projects in Harbor.
You can control the output using pagination flags and format options:
- Use --page and --page-size to navigate through results
- Use --sort to order the results by name, creation time, etc.
- Use -q/--query to filter robots by specific criteria
- Set output-format in your configuration for JSON, YAML, or other formats
Examples:
# List all system robots
harbor-cli robot list
# List system robots with pagination
harbor-cli robot list --page 2 --page-size 20
# List system robots with custom sorting
harbor-cli robot list --sort name
# Filter system robots by name
harbor-cli robot list -q name=ci-robot
# Get robot details in JSON format
harbor-cli robot list --output-format json`,
Args: cobra.MaximumNArgs(0),
Run: func(cmd *cobra.Command, args []string) {
robots, err := api.ListRobot(opts)
if err != nil {
log.Errorf("failed to get robots list: %v", utils.ParseHarborErrorMsg(err))
}
formatFlag := viper.GetString("output-format")
if formatFlag != "" {
err = utils.PrintFormat(robots, formatFlag)
if err != nil {
log.Errorf("Invalid Print Format: %v", err)
}
} else {
list.ListRobots(robots.Payload)
}
},
}
flags := cmd.Flags()
flags.Int64VarP(&opts.Page, "page", "", 1, "Page number")
flags.Int64VarP(&opts.PageSize, "page-size", "", 10, "Size of per page")
flags.StringVarP(&opts.Q, "query", "q", "", "Query string to query resources")
flags.StringVarP(
&opts.Sort,
"sort",
"",
"",
"Sort the resource list in ascending or descending order",
)
return cmd
}

View File

@ -0,0 +1,135 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package robot
import (
"fmt"
"strconv"
"github.com/atotto/clipboard"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/robot/create"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func RefreshSecretCommand() *cobra.Command {
var (
robotID int64
secret string
secretStdin bool
)
cmd := &cobra.Command{
Use: "refresh [robotID]",
Short: "refresh robot secret by id",
Long: `Refresh the secret for an existing robot account in Harbor.
This command generates a new secret for a robot account, effectively revoking
the old secret and requiring updates to any systems using the robot's credentials.
The command supports multiple ways to identify the robot account:
- By providing the robot ID directly as an argument
- Without any arguments, which will prompt for both project and robot selection
You can specify the new secret in several ways:
- Let Harbor generate a random secret (default)
- Provide a custom secret with the --secret flag
- Pipe a secret via stdin using the --secret-stdin flag
After refreshing, the new secret will be:
- Displayed on screen
- Copied to clipboard for immediate use
- Usable immediately for authentication
Important considerations:
- The old secret will be invalidated immediately
- Any systems using the old credentials will need to be updated
- There is no way to recover the old secret after refreshing
Examples:
# Refresh robot secret by ID (generates a random secret)
harbor-cli project robot refresh 123
# Refresh with a custom secret
harbor-cli project robot refresh 123 --secret "MyCustomSecret123"
# Provide secret via stdin (useful for scripting)
echo "MySecretFromScript123" | harbor-cli project robot refresh 123 --secret-stdin
# Interactive refresh (will prompt for project and robot selection)
harbor-cli project robot refresh`,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
var err error
if len(args) == 1 {
robotID, err = strconv.ParseInt(args[0], 10, 64)
if err != nil {
log.Fatalf("failed to parse robot ID: %v", err)
}
} else {
robotID = prompt.GetRobotIDFromUser(-1)
}
if secret != "" {
err = utils.ValidatePassword(secret)
if err != nil {
log.Fatalf("Invalid secret: %v\n", err)
}
}
if secretStdin {
secret = getSecret()
}
response, err := api.RefreshSecret(secret, robotID)
if err != nil {
log.Fatalf("failed to refresh robot secret: %v\n", err)
}
log.Info("Secret updated successfully.")
if response.Payload.Secret != "" {
secret = response.Payload.Secret
create.CreateRobotSecretView("", secret)
err = clipboard.WriteAll(response.Payload.Secret)
if err != nil {
log.Fatalf("failed to write the secret to the clipboard: %v", err)
}
fmt.Println("secret copied to clipboard.")
}
},
}
flags := cmd.Flags()
flags.StringVarP(&secret, "secret", "", "", "secret")
flags.BoolVarP(&secretStdin, "secret-stdin", "", false, "Take the robot secret from stdin")
return cmd
}
// getSecret from commandline
func getSecret() string {
secret, err := utils.GetSecretStdin("Enter your secret: ")
if err != nil {
log.Fatalf("Error reading secret: %v\n", err)
}
if err := utils.ValidatePassword(secret); err != nil {
log.Fatalf("Invalid secret: %v\n", err)
}
return secret
}

View File

@ -0,0 +1,625 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package robot
import (
"fmt"
"strconv"
"github.com/charmbracelet/huh"
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
"github.com/goharbor/harbor-cli/pkg/api"
config "github.com/goharbor/harbor-cli/pkg/config/robot"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/robot/update"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func UpdateRobotCommand() *cobra.Command {
var (
robotID int64
opts update.UpdateView
all bool
configFile string
)
cmd := &cobra.Command{
Use: "update [robotID]",
Short: "update robot by id",
Long: `Update an existing robot account within Harbor.
Robot accounts are non-human users that can be used for automation purposes
such as CI/CD pipelines, scripts, or other automated processes that need
to interact with Harbor. This command allows you to modify an existing robot's
properties including its name, description, duration, and permissions.
This command supports both interactive and non-interactive modes:
- With robot ID: directly updates the specified robot
- Without ID: walks through robot selection interactively
The update process will:
1. Identify the robot account to be updated
2. Load its current configuration
3. Apply the requested changes
4. Save the updated configuration
This command can update both system and project-specific permissions:
- System permissions apply across the entire Harbor instance
- Project permissions apply to specific projects
Configuration can be loaded from:
- Interactive prompts (default)
- Command line flags
- YAML/JSON configuration file
Note: Updating a robot does not regenerate its secret. If you need a new
secret, consider deleting the robot and creating a new one instead.
Examples:
# Update robot by ID with a new description
harbor-cli robot update 123 --description "Updated CI/CD pipeline robot"
# Update robot's duration (extend lifetime)
harbor-cli robot update 123 --duration 180
# Update with all permissions
harbor-cli robot update 123 --all-permission
# Update from configuration file
harbor-cli robot update 123 --robot-config-file ./robot-config.yaml
# Interactive update (will prompt for robot selection and changes)
harbor-cli robot update`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var err error
// Get robot ID from args or interactive prompt
if len(args) == 1 {
robotID, err = strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("failed to parse robot ID: %v", err)
}
} else {
robotID = prompt.GetRobotIDFromUser(-1)
}
// Get current robot configuration
robot, err := api.GetRobot(robotID)
if err != nil {
return fmt.Errorf("failed to get robot: %v", utils.ParseHarborErrorMsg(err))
}
// Initialize update view with current values
bot := robot.Payload
opts.ID = bot.ID
opts.Level = bot.Level
opts.Name = bot.Name
opts.Secret = bot.Secret
opts.Description = bot.Description
opts.Duration = *bot.Duration
opts.Disable = bot.Disable
opts.Editable = bot.Editable
opts.CreationTime = bot.CreationTime
// Extract current permissions (both system and project)
var permissions []models.Permission
var projectPermissionsMap = make(map[string][]models.Permission)
// Separate system and project permissions
for _, perm := range bot.Permissions {
if perm.Kind == "system" && perm.Namespace == "/" {
for _, access := range perm.Access {
permissions = append(permissions, models.Permission{
Resource: access.Resource,
Action: access.Action,
})
}
} else if perm.Kind == "project" {
var projectPerms []models.Permission
for _, access := range perm.Access {
projectPerms = append(projectPerms, models.Permission{
Resource: access.Resource,
Action: access.Action,
})
}
projectPermissionsMap[perm.Namespace] = projectPerms
}
}
logrus.Infof("Loaded robot with %d system permissions and %d project-specific permissions",
len(permissions), len(projectPermissionsMap))
// Handle configuration from file or interactive input
if configFile != "" {
if err := loadFromConfigFileForUpdate(&opts, configFile, &permissions, projectPermissionsMap); err != nil {
return err
}
} else {
if err := handleInteractiveInputForUpdate(&opts, all, &permissions, projectPermissionsMap); err != nil {
return err
}
}
// Build system access permissions
var accessesSystem []*models.Access
for _, perm := range permissions {
accessesSystem = append(accessesSystem, &models.Access{
Resource: perm.Resource,
Action: perm.Action,
})
}
// Build merged permissions structure
opts.Permissions = buildMergedPermissionsForUpdate(projectPermissionsMap, accessesSystem)
// Update robot and handle response
return updateRobotAndHandleResponse(&opts)
},
}
addUpdateFlags(cmd, &opts, &all, &configFile)
return cmd
}
func loadFromConfigFileForUpdate(opts *update.UpdateView, configFile string, permissions *[]models.Permission, projectPermissionsMap map[string][]models.Permission) error {
fmt.Println("Loading configuration from: ", configFile)
loadedOpts, err := config.LoadRobotConfigFromFile(configFile)
if err != nil {
return fmt.Errorf("failed to load robot config from file: %v", err)
}
logrus.Info("Successfully loaded robot configuration")
// Only update fields that should be updated from the config file
// IMPORTANT: Do not update name or level as the Harbor API doesn't allow this
// if loadedOpts.Name != "" {
// opts.Name = loadedOpts.Name
// }
if loadedOpts.Description != "" {
opts.Description = loadedOpts.Description
}
if loadedOpts.Duration != 0 {
opts.Duration = loadedOpts.Duration
}
var systemPermFound bool
for _, perm := range loadedOpts.Permissions {
if perm.Kind == "system" && perm.Namespace == "/" {
systemPermFound = true
for _, access := range perm.Access {
*permissions = append(*permissions, models.Permission{
Resource: access.Resource,
Action: access.Action,
})
}
} else if perm.Kind == "project" {
var projectPerms []models.Permission
for _, access := range perm.Access {
projectPerms = append(projectPerms, models.Permission{
Resource: access.Resource,
Action: access.Action,
})
}
// Validate project permissions before adding
validProjectPerms, err := validateProjectPermissions(projectPerms)
if err != nil {
return err
}
projectPermissionsMap[perm.Namespace] = validProjectPerms
}
}
if !systemPermFound {
return fmt.Errorf("robot configuration must include system-level permissions")
}
logrus.Infof("Loaded robot update with %d system permissions and %d project-specific permissions",
len(*permissions), len(projectPermissionsMap))
return nil
}
func handleInteractiveInputForUpdate(opts *update.UpdateView, all bool, permissions *[]models.Permission, projectPermissionsMap map[string][]models.Permission) error {
// Show interactive form for updating basic details
update.UpdateRobotView(opts)
// Validate duration
if opts.Duration == 0 {
return fmt.Errorf("failed to update robot: %v", utils.ParseHarborErrorMsg(fmt.Errorf("duration cannot be 0")))
}
// Ask if user wants to update permissions
var updatePerms bool
err := huh.NewForm(
huh.NewGroup(
huh.NewSelect[bool]().
Title("Do you want to update permissions?").
Options(
huh.NewOption("No", false),
huh.NewOption("Yes", true),
).
Value(&updatePerms),
),
).WithTheme(huh.ThemeCharm()).WithWidth(60).Run()
if err != nil {
return fmt.Errorf("error asking about permission updates: %v", err)
}
if !updatePerms {
logrus.Info("Keeping existing permissions")
return nil
}
// Get system permissions
if err := getSystemPermissionsForUpdate(all, permissions); err != nil {
return err
}
// Get project permissions
return getProjectPermissionsForUpdate(opts, projectPermissionsMap)
}
func getSystemPermissionsForUpdate(all bool, permissions *[]models.Permission) error {
var updateSystem bool
err := huh.NewForm(
huh.NewGroup(
huh.NewSelect[bool]().
Title("Do you want to update system permissions?").
Options(
huh.NewOption("No", false),
huh.NewOption("Yes", true),
).
Value(&updateSystem),
),
).WithTheme(huh.ThemeCharm()).WithWidth(60).Run()
if err != nil {
return fmt.Errorf("error asking about system permission updates: %v", err)
}
if !updateSystem {
logrus.Info("Keeping existing system permissions")
return nil
}
if all {
perms, _ := api.GetPermissions()
*permissions = nil // Clear existing permissions
for _, perm := range perms.Payload.System {
*permissions = append(*permissions, *perm)
}
} else {
newPermissions := prompt.GetRobotPermissionsFromUser("system")
if len(newPermissions) == 0 {
return fmt.Errorf("failed to update robot: %v",
utils.ParseHarborErrorMsg(fmt.Errorf("no permissions selected, robot account needs at least one permission")))
}
*permissions = newPermissions
}
return nil
}
func getProjectPermissionsForUpdate(opts *update.UpdateView, projectPermissionsMap map[string][]models.Permission) error {
permissionMode, err := promptPermissionModeForUpdate(len(projectPermissionsMap) > 0)
if err != nil {
return fmt.Errorf("error selecting permission mode: %v", err)
}
switch permissionMode {
case "keep":
logrus.Info("Keeping existing project permissions")
return nil
case "clear":
logrus.Info("Clearing all project permissions")
// Clear the map to remove all project permissions
for k := range projectPermissionsMap {
delete(projectPermissionsMap, k)
}
return nil
case "list":
return handleMultipleProjectsPermissionsForUpdate(projectPermissionsMap)
case "per_project":
return handlePerProjectPermissionsForUpdate(projectPermissionsMap)
default:
return fmt.Errorf("unknown permission mode: %s", permissionMode)
}
}
func handleMultipleProjectsPermissionsForUpdate(projectPermissionsMap map[string][]models.Permission) error {
// First, decide whether to replace or keep existing project permissions
if len(projectPermissionsMap) > 0 {
var replaceExisting bool
err := huh.NewForm(
huh.NewGroup(
huh.NewSelect[bool]().
Title("What do you want to do with existing project permissions?").
Options(
huh.NewOption("Keep existing and add new", false),
huh.NewOption("Replace all existing with new selection", true),
).
Value(&replaceExisting),
),
).WithTheme(huh.ThemeCharm()).WithWidth(60).Run()
if err != nil {
return fmt.Errorf("error asking about existing permissions: %v", err)
}
if replaceExisting {
// Clear the map to remove all project permissions
for k := range projectPermissionsMap {
delete(projectPermissionsMap, k)
}
}
}
selectedProjects, err := getMultipleProjectsFromUser()
if err != nil {
return fmt.Errorf("error selecting projects: %v", err)
}
if len(selectedProjects) > 0 {
fmt.Println("Select permissions to apply to all selected projects:")
projectPermissions := prompt.GetRobotPermissionsFromUser("project")
// Validate project permissions
validProjectPerms, err := validateProjectPermissions(projectPermissions)
if err != nil {
return err
}
for _, projectName := range selectedProjects {
projectPermissionsMap[projectName] = validProjectPerms
}
}
return nil
}
func handlePerProjectPermissionsForUpdate(projectPermissionsMap map[string][]models.Permission) error {
// First, decide whether to replace or keep existing project permissions
if len(projectPermissionsMap) > 0 {
var modifyMode string
err := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("How do you want to modify project permissions?").
Options(
huh.NewOption("Add new projects only", "add"),
huh.NewOption("Modify existing projects", "modify"),
huh.NewOption("Replace all existing with new projects", "replace"),
).
Value(&modifyMode),
),
).WithTheme(huh.ThemeCharm()).WithWidth(60).Run()
if err != nil {
return fmt.Errorf("error asking about permission modification: %v", err)
}
if modifyMode == "replace" {
// Clear the map to remove all project permissions
for k := range projectPermissionsMap {
delete(projectPermissionsMap, k)
}
} else if modifyMode == "modify" {
// Show existing projects and let user select which to modify
var existingProjects []string
for project := range projectPermissionsMap {
existingProjects = append(existingProjects, project)
}
var selectedProjects []string
var projectOptions []huh.Option[string]
for _, p := range existingProjects {
projectOptions = append(projectOptions, huh.NewOption(p, p))
}
err = huh.NewForm(
huh.NewGroup(
huh.NewMultiSelect[string]().
Title("Select projects to modify").
Options(projectOptions...).
Value(&selectedProjects),
),
).WithTheme(huh.ThemeCharm()).WithWidth(80).Run()
if err != nil {
return fmt.Errorf("error selecting projects to modify: %v", err)
}
// Update permissions for selected projects
for _, project := range selectedProjects {
fmt.Printf("Updating permissions for project: %s\n", project)
projectPerms := prompt.GetRobotPermissionsFromUser("project")
// Validate project permissions
validProjectPerms, err := validateProjectPermissions(projectPerms)
if err != nil {
return err
}
projectPermissionsMap[project] = validProjectPerms
}
return nil
}
}
// Add new projects
for {
projectName, err := prompt.GetProjectNameFromUser()
if err != nil {
return fmt.Errorf("%v", utils.ParseHarborErrorMsg(err))
}
if projectName == "" {
return fmt.Errorf("project name cannot be empty")
}
projectPerms := prompt.GetRobotPermissionsFromUser("project")
// Validate project permissions
validProjectPerms, err := validateProjectPermissions(projectPerms)
if err != nil {
return err
}
projectPermissionsMap[projectName] = validProjectPerms
moreProjects, err := promptMoreProjects()
if err != nil {
return fmt.Errorf("error asking for more projects: %v", err)
}
if !moreProjects {
break
}
}
return nil
}
// validateProjectPermissions filters out permissions that are not valid for projects
func validateProjectPermissions(permissions []models.Permission) ([]models.Permission, error) {
perms, err := api.GetPermissions()
if err != nil {
return nil, fmt.Errorf("failed to get valid permissions: %v", err)
}
// Create a map of valid project permissions
validProjectPerms := make(map[string]bool)
for _, perm := range perms.Payload.Project {
key := fmt.Sprintf("%s:%s", perm.Resource, perm.Action)
validProjectPerms[key] = true
}
// Filter the permissions
var validPerms []models.Permission
var invalidPerms []string
for _, perm := range permissions {
key := fmt.Sprintf("%s:%s", perm.Resource, perm.Action)
if validProjectPerms[key] {
validPerms = append(validPerms, perm)
} else {
invalidPerms = append(invalidPerms, key)
}
}
// Warn about invalid permissions
if len(invalidPerms) > 0 {
logrus.Warnf("Removed %d invalid project permissions: %v", len(invalidPerms), invalidPerms)
}
return validPerms, nil
}
func buildMergedPermissionsForUpdate(projectPermissionsMap map[string][]models.Permission, accessesSystem []*models.Access) []*update.RobotPermission {
var mergedPermissions []*update.RobotPermission
// Add project permissions
for projectName, projectPermissions := range projectPermissionsMap {
var accessesProject []*models.Access
for _, perm := range projectPermissions {
accessesProject = append(accessesProject, &models.Access{
Resource: perm.Resource,
Action: perm.Action,
})
}
if len(accessesProject) > 0 {
mergedPermissions = append(mergedPermissions, &update.RobotPermission{
Namespace: projectName,
Access: accessesProject,
Kind: "project",
})
}
}
if len(accessesSystem) > 0 {
// Add system permissions only if there are any
mergedPermissions = append(mergedPermissions, &update.RobotPermission{
Namespace: "/",
Access: accessesSystem,
Kind: "system",
})
}
return mergedPermissions
}
func updateRobotAndHandleResponse(opts *update.UpdateView) error {
err := api.UpdateRobot(opts)
if err != nil {
return fmt.Errorf("failed to update robot: %v", utils.ParseHarborErrorMsg(err))
}
logrus.Infof("Successfully updated robot account '%s' (ID: %d)", opts.Name, opts.ID)
// Handle output format
if formatFlag := viper.GetString("output-format"); formatFlag != "" {
res, _ := api.GetRobot(opts.ID)
utils.SavePayloadJSON(opts.Name, res.Payload)
}
return nil
}
func addUpdateFlags(cmd *cobra.Command, opts *update.UpdateView, all *bool, configFile *string) {
flags := cmd.Flags()
flags.BoolVarP(all, "all-permission", "a", false, "Select all permissions for the robot account")
flags.StringVarP(&opts.Name, "name", "", "", "name of the robot account")
flags.StringVarP(&opts.Description, "description", "", "", "description of the robot account")
flags.Int64VarP(&opts.Duration, "duration", "", 0, "set expiration of robot account in days")
flags.StringVarP(configFile, "robot-config-file", "r", "", "YAML/JSON file with robot configuration")
}
func promptPermissionModeForUpdate(hasExistingProjectPerms bool) (string, error) {
var permissionMode string
var options []huh.Option[string]
if hasExistingProjectPerms {
options = []huh.Option[string]{
huh.NewOption("Keep existing project permissions", "keep"),
huh.NewOption("Clear all project permissions", "clear"),
huh.NewOption("Per Project (individual permissions)", "per_project"),
huh.NewOption("List (same permissions for multiple projects)", "list"),
}
} else {
options = []huh.Option[string]{
huh.NewOption("No project permissions (system-level only)", "clear"),
huh.NewOption("Per Project (individual permissions)", "per_project"),
huh.NewOption("List (same permissions for multiple projects)", "list"),
}
}
err := huh.NewForm(
huh.NewGroup(
huh.NewNote().
Title("Project Permission Mode").
Description("Select how you want to handle project permissions:"),
huh.NewSelect[string]().
Title("Permission Mode").
Options(options...).
Value(&permissionMode),
),
).WithTheme(huh.ThemeCharm()).WithWidth(60).WithHeight(10).Run()
return permissionMode, err
}

View File

@ -0,0 +1,87 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package robot
import (
"strconv"
"github.com/goharbor/go-client/pkg/sdk/v2.0/client/robot"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/views/robot/view"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func ViewRobotCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "view [robotID]",
Short: "get robot by id",
Long: `View detailed information about a robot account in Harbor.
This command displays comprehensive information about a robot account including
its ID, name, description, creation time, expiration, and the permissions
it has been granted. Supports both system-level and project-level robot accounts.
The command supports multiple ways to identify the robot account:
- By providing the robot ID directly as an argument
- Without any arguments, which will prompt for robot selection
The displayed information includes:
- Basic details (ID, name, description)
- Temporal information (creation date, expiration date, remaining time)
- Security details (disabled status)
- Detailed permissions breakdown by resource and action
- For system robots: permissions across multiple projects are shown separately
System-level robots can have permissions spanning multiple projects, while
project-level robots are scoped to a single project.
Examples:
# View robot by ID
harbor-cli robot view 123
# Interactive selection (will prompt for robot)
harbor-cli robot view`,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
var (
robot *robot.GetRobotByIDOK
robotID int64
err error
)
if len(args) == 1 {
robotID, err = strconv.ParseInt(args[0], 10, 64)
if err != nil {
log.Fatalf("failed to parse robot ID: %v", err)
}
} else {
robotID = prompt.GetRobotIDFromUser(-1)
}
robot, err = api.GetRobot(robotID)
if err != nil {
log.Fatalf("failed to get robot: %v", err)
}
// Convert to a list and display
// robots := &models.Robot{robot.Payload}
view.ViewRobot(robot.Payload)
},
}
return cmd
}

View File

@ -0,0 +1,33 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scan_all
import "github.com/spf13/cobra"
func ScanAll() *cobra.Command {
cmd := &cobra.Command{
Use: "scan-all",
Short: "Scan all artifacts",
}
cmd.AddCommand(
UpdateScanAllScheduleCommand(),
StopScanAllCommand(),
ViewScanAllScheduleCommand(),
GetScanAllMetricsCommand(),
RunScanAllCommand(),
)
return cmd
}

View File

@ -0,0 +1,83 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scan_all
import (
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/utils"
view "github.com/goharbor/harbor-cli/pkg/views/scan-all/metrics"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func GetScanAllMetricsCommand() *cobra.Command {
var scheduled bool
cmd := &cobra.Command{
Use: "metrics",
Short: "Get the metrics of the latest scan all process",
Long: `Display comprehensive metrics about the most recent vulnerability scan execution.
This command retrieves and displays detailed statistics about the most recent scan all
process in Harbor, including:
- Running: Number of currently running scan tasks
- Success: Number of successfully completed scan tasks
- Error: Number of failed scan tasks
- Completed: Total number of completed scan tasks
- Total: Total number of scan tasks
- Ongoing: Whether the scan is still in progress
- Trigger: What triggered the scan (Manual, Scheduled, etc.)
The metrics provide visibility into the progress and results of vulnerability scanning across your Harbor registry.
Examples:
# Get metrics for the latest scan
harbor-cli scan-all metrics
# Get metrics for the latest scheduled scan
harbor-cli scan-all metrics --scheduled
# Display metrics in JSON format
harbor-cli scan-all metrics --output-format json`,
Args: cobra.MaximumNArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
logrus.Info("Retrieving scan all metrics")
metrics, err := api.GetScanAllMetrics(scheduled)
if err != nil {
logrus.Errorf("Failed to retrieve scan all metrics: %v", utils.ParseHarborErrorMsg(err))
return err
}
FormatFlag := viper.GetString("output-format")
if FormatFlag != "" {
err = utils.PrintFormat(metrics, FormatFlag)
if err != nil {
return err
}
} else {
view.ViewScanMetrics(metrics)
}
return nil
},
}
flags := cmd.Flags()
// latest scheduled metrics is deprecated in the API
flags.BoolVarP(&scheduled, "scheduled", "s", false, "Get the metrics of the latest scheduled scan all process")
return cmd
}

View File

@ -0,0 +1,71 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scan_all
import (
"fmt"
"github.com/go-openapi/strfmt"
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func RunScanAllCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "run",
Short: "Scan all artifacts now",
Long: `Initiate an immediate vulnerability scan of all artifacts in Harbor.
This command triggers a manual scan of all artifacts across all projects in your Harbor instance.
The scan will check for known vulnerabilities in container images using the configured scanner(s).
The scan runs asynchronously in the background. After initiating the scan, you can:
- Check the scan progress with 'harbor-cli scan-all metrics'
- View results through the Harbor UI
Important considerations:
- This operation can be resource intensive on large registries
- Scanning many artifacts simultaneously may impact system performance
- The time to complete depends on the number and size of artifacts
- Only one scan-all operation can run at a time
Examples:
# Start scanning all artifacts immediately
harbor-cli scan-all run
# Start scanning and monitor progress
harbor-cli scan-all run && watch -n 0.2 harbor-cli scan-all metrics
The scan progress and results can be monitored through the metrics command
or through the Harbor web interface.`,
Args: cobra.MaximumNArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
logrus.Info("Initiating manual scan of all artifacts")
// Random cron expression and random time need to be passed to the API, even though they are not used, otherwise it returns bad request
randomCron := "0 * * * * *"
randomTime := strfmt.DateTime{}
err := api.CreateScanAllSchedule(models.ScheduleObj{Type: "Manual", Cron: randomCron, NextScheduledTime: randomTime})
if err != nil {
return fmt.Errorf("failed to start scan all operation: %v", utils.ParseHarborErrorMsg(err))
}
logrus.Info("Successfully started scan all operation")
return nil
},
}
return cmd
}

View File

@ -0,0 +1,52 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scan_all
import (
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func StopScanAllCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "stop",
Short: "Stop scanning all artifacts",
Long: `Stop an ongoing vulnerability scan of all artifacts in Harbor.
This command halts the current scan-all operation that was either manually triggered
or scheduled. When stopped, scans that are already in progress will complete, but no new artifacts will be scanned. The scan can be restarted later using the 'scan-all run' command.
Examples:
# Stop the current scan-all operation
harbor-cli scan-all stop
# Stop and then check metrics to confirm
harbor-cli scan-all stop && harbor-cli scan-all metrics`,
Args: cobra.MaximumNArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
logrus.Info("Stopping scan all operation")
err := api.StopScanAll()
if err != nil {
logrus.Errorf("Failed to stop scan all operation: %v", utils.ParseHarborErrorMsg(err))
return err
}
logrus.Info("Successfully stopped scan all operation")
return nil
},
}
return cmd
}

View File

@ -0,0 +1,189 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scan_all
import (
"errors"
"fmt"
"strings"
"github.com/go-openapi/strfmt"
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/scan-all/update"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
var validScheduleTypes = map[string]bool{
"None": true,
"Hourly": true,
"Daily": true,
"Weekly": true,
"Custom": true,
}
func UpdateScanAllScheduleCommand() *cobra.Command {
var scheduleType string
var cron string
cmd := &cobra.Command{
Use: "update-schedule",
Short: "update-schedule [schedule-type: none|hourly|daily|weekly|custom]",
Long: `Configure or update the automatic vulnerability scan schedule for all artifacts.
This command allows you to set when Harbor automatically scans all artifacts for vulnerabilities. You can choose from predefined schedules or create a custom schedule using cron expressions.
Available schedule types:
- none: Disable automatic scanning
- hourly: Run scan every hour
- daily: Run scan once per day
- weekly: Run scan once per week
- custom: Define a custom schedule using a cron expression
For custom schedules, Harbor requires a 6-field cron expression in the format:
seconds minutes hours day-of-month month day-of-week
Examples:
# Disable scheduled scanning
harbor-cli scan-all update-schedule none
# Set daily automatic scanning
harbor-cli scan-all update-schedule daily
# Set weekly automatic scanning
harbor-cli scan-all update-schedule weekly
# Set a custom schedule (every day at 2:30 AM)
harbor-cli scan-all update-schedule custom --cron "0 30 2 * * *"
# Use interactive mode to configure a custom schedule
harbor-cli scan-all update-schedule custom
Note: For custom schedules, if you provide a 5-field cron expression, the CLI will automatically add a leading "0" for the seconds field to create the required 6-field format.`,
Aliases: []string{"us"},
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
scheduleType = cases.Title(language.English).String(strings.ToLower(args[0]))
if !validScheduleTypes[scheduleType] {
return fmt.Errorf("invalid schedule type: %s. Valid types are: none, hourly, daily, weekly, custom", args[0])
}
logrus.Infof("Updating scan all schedule to type: %s", scheduleType)
switch scheduleType {
case "None":
return updateScheduleToNone()
case "Hourly", "Daily", "Weekly":
return updatePredefinedSchedule(scheduleType)
case "Custom":
return updateCustomSchedule(cron)
}
return nil
},
}
flags := cmd.Flags()
flags.StringVar(&cron, "cron", "", "Cron expression for custom schedule (include the expression in double quotes)")
return cmd
}
func updateScheduleToNone() error {
logrus.Info("Setting scan all schedule to None (disabled)")
err := api.UpdateScanAllSchedule(models.ScheduleObj{Type: "None"})
if err != nil {
return fmt.Errorf("failed to disable scan schedule: %v", utils.ParseHarborErrorMsg(err))
}
logrus.Info("Successfully disabled scan all schedule")
return nil
}
func updatePredefinedSchedule(scheduleType string) error {
logrus.Infof("Setting scan all schedule to %s", scheduleType)
// Random cron expression and time needed by API
randomCron := "0 0 * * * * "
randomTime := strfmt.DateTime{}
err := api.UpdateScanAllSchedule(models.ScheduleObj{
Type: scheduleType,
Cron: randomCron,
NextScheduledTime: randomTime,
})
if err != nil {
return fmt.Errorf("failed to update scan schedule: %v", utils.ParseHarborErrorMsg(err))
}
logrus.Infof("Successfully set scan all schedule to %s", scheduleType)
return nil
}
func updateCustomSchedule(cron string) error {
if cron == "" {
logrus.Info("Opening interactive form for custom schedule configuration")
update.UpdateSchedule(&cron)
}
if err := validateCron(cron); err != nil {
return err
}
logrus.Infof("Setting scan all schedule with custom cron expression: %s", cron)
// Random time needed by API
randomTime := strfmt.DateTime{}
err := api.UpdateScanAllSchedule(models.ScheduleObj{
Type: "Custom",
Cron: cron,
NextScheduledTime: randomTime,
})
if err != nil {
errMsg := utils.ParseHarborErrorMsg(err)
if strings.Contains(errMsg, "400") {
return fmt.Errorf("invalid cron expression: Harbor rejected the schedule. Use the standard 5-field format (minute hour day month weekday)")
}
return fmt.Errorf("failed to update scan schedule: %v", errMsg)
}
logrus.Info("Successfully set scan all schedule with custom cron expression")
return nil
}
func validateCron(cron string) error {
if cron == "" {
return errors.New("cron expression cannot be empty")
}
fields := strings.Fields(cron)
if len(fields) < 6 {
if len(fields) == 5 {
logrus.Infof("Converting 5-field cron to 6-field by adding '0' for seconds")
return fmt.Errorf("harbor requires 6-field cron format (including seconds). Try: '0 %s'", cron)
}
return fmt.Errorf("harbor requires 6-field cron format (seconds minute hour day month weekday)")
}
if len(fields) > 6 {
return fmt.Errorf("too many fields in cron expression, expected 6 but got %d", len(fields))
}
return nil
}

View File

@ -0,0 +1,74 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scan_all
import (
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/scan-all/view-schedule"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// This command does not work because the API does not return the response body
// API: https://demo.goharbor.io/devcenter-api-2.0
func ViewScanAllScheduleCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "view-schedule",
Short: "View the scan all schedule",
Long: `Display the current vulnerability scan schedule configuration.
This command retrieves and shows the current automatic scanning schedule settings for your Harbor instance, including:
- Schedule Type: The type of schedule (None, Hourly, Daily, Weekly, or Custom)
- Cron Expression: For custom schedules, shows the configured cron pattern
- Next Scheduled Time: When the next automatic scan is scheduled to run
This information helps you understand when Harbor will automatically scan your artifacts
for vulnerabilities.
Examples:
# View the current scan schedule
harbor-cli scan-all view-schedule
# View the schedule in JSON format
harbor-cli scan-all view-schedule --output-format json
You can use this command to verify changes after updating the schedule with the 'update-schedule' command.`,
Args: cobra.MaximumNArgs(0),
Aliases: []string{"vs"},
RunE: func(cmd *cobra.Command, args []string) error {
logrus.Info("Retrieving scan all schedule configuration")
schedule, err := api.GetScanAllSchedule()
if err != nil {
logrus.Errorf("Failed to retrieve scan all schedule: %v", utils.ParseHarborErrorMsg(err))
return err
}
FormatFlag := viper.GetString("output-format")
if FormatFlag != "" {
err = utils.PrintFormat(schedule, FormatFlag)
if err != nil {
return err
}
} else {
view.ViewScanSchedule(schedule)
}
return nil
},
}
return cmd
}

View File

@ -0,0 +1,35 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scanner
import "github.com/spf13/cobra"
func Scanner() *cobra.Command {
cmd := &cobra.Command{
Use: "scanner",
Short: "scanner commands",
}
cmd.AddCommand(
CreateScannerCommand(),
ListScannerCommand(),
ViewCommand(),
MetadataCommand(),
SetDefaultCommand(),
DeleteCommand(),
UpdateCommand(),
)
return cmd
}

View File

@ -0,0 +1,64 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scanner
import (
"fmt"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/views/scanner/create"
"github.com/spf13/cobra"
)
func CreateScannerCommand() *cobra.Command {
var opts create.CreateView
var ping bool
cmd := &cobra.Command{
Use: "create",
Short: "Create a scanner",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
if opts.Name == "" || opts.Auth == "" || opts.URL == "" {
create.CreateScannerView(&opts)
}
if ping {
err := api.PingScanner(opts)
if err != nil {
return fmt.Errorf("failed to ping the scanner adapter: %v", err)
}
} else {
err := api.CreateScanner(opts)
if err != nil {
return fmt.Errorf("failed to create scanner: %v", err.Error())
}
}
return nil
},
}
flags := cmd.Flags()
flags.StringVar(&opts.Name, "name", "", "New name for the scanner")
flags.StringVar(&opts.Description, "description", "", "New description for the scanner")
flags.StringVar(&opts.Auth, "auth", "", "Authentication method [None|Basic|Bearer|X-ScannerAdapter-API-Key]")
flags.StringVar(&opts.AccessCredential, "credential", "", "Authorization header for the Scanner Adapter API")
flags.StringVar(&opts.URL, "url", "", "Base URL of the scanner adapter")
flags.BoolVar(&opts.Disabled, "disabled", false, "Disable the scanner registration")
flags.BoolVar(&opts.SkipCertVerify, "skip-cert-verification", false, "Skip certificate verification in HTTP requests")
flags.BoolVar(&opts.UseInternalAddr, "use-internal-addr", false, "Use internal registry address for scanning")
flags.BoolVarP(&ping, "ping", "", false, "Ping the scanner adapter without creating it.")
return cmd
}

View File

@ -0,0 +1,66 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scanner
import (
"fmt"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func DeleteCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "delete [scanner-name]",
Short: "Delete a scanner registration",
Long: `Delete a scanner registration from Harbor.
You can:
- Provide the scanner name as an argument to delete it directly, or
- Omit the argument to select a scanner interactively.
Note: Deleting a scanner will permanently remove its registration and associated metadata from the system.
Examples:
# Delete a scanner by name
harbor scanner delete trivy-scanner
# Interactively choose a scanner to delete
harbor scanner delete`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var err error
var registrationID string
if len(args) > 0 {
scanner, err := api.GetScannerByName(args[0])
if err != nil {
return fmt.Errorf("failed to retrieve scanner by name %q: %v", args[0], err)
}
registrationID = scanner.UUID
} else {
registrationID = prompt.GetScannerIdFromUser()
}
err = api.DeleteScanner(registrationID)
if err != nil {
return fmt.Errorf("failed to delete scanner: %v", err)
}
log.Infof("Scanner deleted successfully")
return nil
},
}
return cmd
}

View File

@ -0,0 +1,58 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scanner
import (
"fmt"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/utils"
list "github.com/goharbor/harbor-cli/pkg/views/scanner/list"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func ListScannerCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List scanners",
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
scannersResp, err := api.ListScanners()
if err != nil {
return fmt.Errorf("failed to list scanners: %v", err)
}
scanners := scannersResp.Payload
if len(scanners) == 0 {
log.Info("No scanners found")
return nil
}
formatFlag := viper.GetString("output-format")
if formatFlag != "" {
err = utils.PrintFormat(scanners, formatFlag)
if err != nil {
return err
}
} else {
list.ListScanners(scanners)
}
return nil
},
}
return cmd
}

Some files were not shown because too many files have changed in this diff Show More