Compare commits

...

76 Commits
v0.0.5 ... 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
438 changed files with 23918 additions and 1307 deletions

1
.dagger/.gitignore vendored
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/)

View File

@ -1,23 +1,23 @@
module dagger/harbor-cli
go 1.23.1
go 1.24.4
require (
github.com/99designs/gqlgen v0.17.70
github.com/Khan/genqlient v0.8.0
github.com/vektah/gqlparser/v2 v2.5.23
go.opentelemetry.io/otel v1.34.0
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.34.0
go.opentelemetry.io/otel/sdk v1.35.0
go.opentelemetry.io/otel/sdk/log v0.8.0
go.opentelemetry.io/otel/trace v1.34.0
go.opentelemetry.io/otel/trace v1.35.0
go.opentelemetry.io/proto/otlp v1.3.1
golang.org/x/sync v0.12.0
google.golang.org/grpc v1.71.0
golang.org/x/sync v0.15.0
google.golang.org/grpc v1.73.0
)
require (
@ -31,13 +31,13 @@ require (
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.34.0
go.opentelemetry.io/otel/sdk/metric v1.34.0
golang.org/x/net v0.38.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // 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
)

View File

@ -1,7 +1,7 @@
github.com/99designs/gqlgen v0.17.70 h1:xgLIgQuG+Q2L/AE9cW595CT7xCWCe/bpPIFGSfsGSGs=
github.com/99designs/gqlgen v0.17.70/go.mod h1:fvCiqQAu2VLhKXez2xFvLmE47QgAPf/KTPN5XQ4rsHQ=
github.com/Khan/genqlient v0.8.0 h1:Hd1a+E1CQHYbMEKakIkvBH3zW0PWEeiX6Hp1i2kP2WE=
github.com/Khan/genqlient v0.8.0/go.mod h1:hn70SpYjWteRGvxTwo0kfaqg4wxvndECGkfa1fdDdYI=
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=
@ -15,8 +15,8 @@ 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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/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=
@ -29,12 +29,12 @@ github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq
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.23 h1:PurJ9wpgEVB7tty1seRUwkIDa/QH5RzkzraiKIjKLfA=
github.com/vektah/gqlparser/v2 v2.5.23/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
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.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
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=
@ -51,34 +51,34 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9
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.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
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.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
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.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24=
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
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=

View File

@ -24,9 +24,8 @@ import (
const (
GOLANGCILINT_VERSION = "v2.1.2"
GO_VERSION = "1.24.2"
SYFT_VERSION = "v1.9.0"
GORELEASER_VERSION = "v2.3.2"
GO_VERSION = "1.24.4"
GORELEASER_VERSION = "v2.8.2"
)
func New(
@ -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:

View File

@ -1,5 +1,5 @@
version: 2
project_name: harbor
project_name: harbor-cli
before:
hooks:
@ -63,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. 🙌**

118
README.md
View File

@ -1,37 +1,76 @@
![harbor-3](https://github.com/goharbor/harbor-cli/assets/70086051/835ab686-1cce-4ac7-bc57-05a35c2b73cc)
**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 Logo_256px](https://github.com/user-attachments/assets/fa18e8f0-a2e4-4462-ab2d-446a88f9edb3)
**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,16 +117,23 @@ 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:
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
@ -103,15 +149,19 @@ Use "harbor [command] --help" for more information about a command.
#### Config Management
##### Hierarchy
Use the `--config` flag to specify a custom configuration file path (highest priority).
```bash
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
@ -178,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
@ -196,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.
@ -226,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

@ -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

@ -30,8 +30,18 @@ func ListArtifactCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "list artifacts within a repository",
Args: cobra.MaximumNArgs(1),
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")
@ -41,9 +51,15 @@ func ListArtifactCommand() *cobra.Command {
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)
}

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,17 +15,26 @@ package root
import (
"fmt"
"io"
"time"
"github.com/goharbor/harbor-cli/cmd/harbor/root/artifact"
"github.com/goharbor/harbor-cli/cmd/harbor/root/config"
"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"
@ -58,17 +67,16 @@ harbor help
utils.InitConfig(cfgFile, userSpecifiedConfig)
// Conditionally set the timestamp format only in verbose mode
formatter := &logrus.TextFormatter{}
if verbose {
logrus.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
TimestampFormat: time.RFC3339,
})
formatter.FullTimestamp = true
formatter.TimestampFormat = time.RFC3339
logrus.SetLevel(logrus.DebugLevel)
} else {
// No timestamp format for non-verbose
logrus.SetFormatter(&logrus.TextFormatter{
DisableTimestamp: true,
})
logrus.SetOutput(io.Discard)
}
logrus.SetFormatter(formatter)
return nil
},
@ -88,21 +96,102 @@ harbor help
fmt.Println(err.Error())
}
root.AddCommand(
versionCommand(),
LoginCommand(),
config.Config(),
project.Project(),
registry.Registry(),
repositry.Repository(),
user.User(),
artifact.Artifact(),
tag.TagCommand(),
HealthCommand(),
schedule.Schedule(),
labels.Labels(),
InfoCommand(),
)
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

@ -11,43 +11,44 @@
// 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 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_ConfigCmd(t *testing.T) {
func Test_ContextCmd(t *testing.T) {
tempDir := t.TempDir()
data := Initialize(t, tempDir)
defer ConfigCleanup(t, data)
SetMockKeyring(t)
data := helpers.Initialize(t, tempDir)
defer helpers.ConfigCleanup(t, data)
helpers.SetMockKeyring(t)
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"config"})
rootCmd.SetArgs([]string{"context"})
err := rootCmd.Execute()
assert.Nil(t, err)
}
func Test_ConfigListCmd(t *testing.T) {
func Test_ContextListCmd(t *testing.T) {
tempDir := t.TempDir()
data := Initialize(t, tempDir)
defer ConfigCleanup(t, data)
SetMockKeyring(t)
data := helpers.Initialize(t, tempDir)
defer helpers.ConfigCleanup(t, data)
helpers.SetMockKeyring(t)
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"config", "list"})
rootCmd.SetArgs([]string{"context", "list"})
err := rootCmd.Execute()
assert.Nil(t, err)
}
func Test_ConfigGetCmd_Success(t *testing.T) {
func Test_ContextGetCmd_Success(t *testing.T) {
tempDir := t.TempDir()
data := Initialize(t, tempDir)
defer ConfigCleanup(t, data)
SetMockKeyring(t)
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{
@ -64,16 +65,16 @@ func Test_ConfigGetCmd_Success(t *testing.T) {
t.Fatal(err)
}
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"config", "get", "credentials.serveraddress"})
rootCmd.SetArgs([]string{"context", "get", "credentials.serveraddress"})
err = rootCmd.Execute()
assert.NoError(t, err)
}
func Test_ConfigGetCmd_Failure(t *testing.T) {
func Test_ContextGetCmd_Failure(t *testing.T) {
tempDir := t.TempDir()
data := Initialize(t, tempDir)
defer ConfigCleanup(t, data)
SetMockKeyring(t)
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{
@ -90,16 +91,16 @@ func Test_ConfigGetCmd_Failure(t *testing.T) {
t.Fatal(err)
}
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"config", "get", "serveraddress"})
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_ConfigGetCmd_CredentialName_Success(t *testing.T) {
func Test_ContextGetCmd_CredentialName_Success(t *testing.T) {
tempDir := t.TempDir()
data := Initialize(t, tempDir)
defer ConfigCleanup(t, data)
SetMockKeyring(t)
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{
@ -116,16 +117,16 @@ func Test_ConfigGetCmd_CredentialName_Success(t *testing.T) {
t.Fatal(err)
}
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"config", "get", "credentials.serveraddress", "--name", "harbor-cli@http://demo.goharbor.io"})
rootCmd.SetArgs([]string{"context", "get", "credentials.serveraddress", "--name", "harbor-cli@http://demo.goharbor.io"})
err = rootCmd.Execute()
assert.NoError(t, err)
}
func Test_ConfigGetCmd_CredentialName_Failure(t *testing.T) {
func Test_ContextGetCmd_CredentialName_Failure(t *testing.T) {
tempDir := t.TempDir()
data := Initialize(t, tempDir)
defer ConfigCleanup(t, data)
SetMockKeyring(t)
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{
@ -142,16 +143,16 @@ func Test_ConfigGetCmd_CredentialName_Failure(t *testing.T) {
t.Fatal(err)
}
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"config", "get", "credentials.serveraddress", "--name", "harbor-cli@http://goharbor.io"})
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_ConfigUpdateCmd_Success(t *testing.T) {
func Test_ContextUpdateCmd_Success(t *testing.T) {
tempDir := t.TempDir()
data := Initialize(t, tempDir)
defer ConfigCleanup(t, data)
SetMockKeyring(t)
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{
@ -168,16 +169,16 @@ func Test_ConfigUpdateCmd_Success(t *testing.T) {
t.Fatal(err)
}
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"config", "update", "credentials.serveraddress", "http://demo.goharbor.io"})
rootCmd.SetArgs([]string{"context", "update", "credentials.serveraddress", "http://demo.goharbor.io"})
err = rootCmd.Execute()
assert.NoError(t, err)
}
func Test_ConfigUpdateCmd_CredentialName_Success(t *testing.T) {
func Test_ContextUpdateCmd_CredentialName_Success(t *testing.T) {
tempDir := t.TempDir()
data := Initialize(t, tempDir)
defer ConfigCleanup(t, data)
SetMockKeyring(t)
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{
@ -194,16 +195,16 @@ func Test_ConfigUpdateCmd_CredentialName_Success(t *testing.T) {
t.Fatal(err)
}
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"config", "update", "credentials.serveraddress", "http://demo.goharbor.io", "--name", "harbor-cli@http://demo.goharbor.io"})
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_ConfigUpdateCmd_CredentialName_Failure(t *testing.T) {
func Test_ContextUpdateCmd_CredentialName_Failure(t *testing.T) {
tempDir := t.TempDir()
data := Initialize(t, tempDir)
defer ConfigCleanup(t, data)
SetMockKeyring(t)
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{
@ -220,16 +221,16 @@ func Test_ConfigUpdateCmd_CredentialName_Failure(t *testing.T) {
t.Fatal(err)
}
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"config", "update", "credentials.serveraddress", "http://demo.goharbor.io", "--name", "harbor-cli@http://goharbor.io"})
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_ConfigUpdateCmd_Failure(t *testing.T) {
func Test_ContextUpdateCmd_Failure(t *testing.T) {
tempDir := t.TempDir()
data := Initialize(t, tempDir)
defer ConfigCleanup(t, data)
SetMockKeyring(t)
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{
@ -246,16 +247,16 @@ func Test_ConfigUpdateCmd_Failure(t *testing.T) {
t.Fatal(err)
}
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"config", "update", "serveraddress", "http://demo.goharbor.io"})
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_ConfigDeleteCmd_Success(t *testing.T) {
func Test_ContextDeleteCmd_Success(t *testing.T) {
tempDir := t.TempDir()
data := Initialize(t, tempDir)
defer ConfigCleanup(t, data)
SetMockKeyring(t)
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{
@ -272,7 +273,7 @@ func Test_ConfigDeleteCmd_Success(t *testing.T) {
t.Fatal(err)
}
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"config", "delete", "credentials.serveraddress"})
rootCmd.SetArgs([]string{"context", "delete", "credentials.serveraddress"})
err = rootCmd.Execute()
assert.NoError(t, err)
config, err := utils.GetCurrentHarborConfig()
@ -282,11 +283,11 @@ func Test_ConfigDeleteCmd_Success(t *testing.T) {
assert.Empty(t, config.Credentials[0].ServerAddress)
}
func Test_ConfigDeleteCmd_Failure(t *testing.T) {
func Test_ContextDeleteCmd_Failure(t *testing.T) {
tempDir := t.TempDir()
data := Initialize(t, tempDir)
defer ConfigCleanup(t, data)
SetMockKeyring(t)
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{
@ -303,16 +304,16 @@ func Test_ConfigDeleteCmd_Failure(t *testing.T) {
t.Fatal(err)
}
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"config", "delete", "serveraddress"})
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_ConfigDeleteCmd_CredentialName_Success(t *testing.T) {
func Test_ContextDeleteCmd_CredentialName_Success(t *testing.T) {
tempDir := t.TempDir()
data := Initialize(t, tempDir)
defer ConfigCleanup(t, data)
SetMockKeyring(t)
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{
@ -329,7 +330,7 @@ func Test_ConfigDeleteCmd_CredentialName_Success(t *testing.T) {
t.Fatal(err)
}
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"config", "delete", "credentials.serveraddress", "--name", "harbor-cli@http://demo.goharbor.io"})
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()
@ -339,11 +340,11 @@ func Test_ConfigDeleteCmd_CredentialName_Success(t *testing.T) {
assert.Empty(t, config.Credentials[0].ServerAddress)
}
func Test_ConfigDeleteCmd_CredentialName_Failure(t *testing.T) {
func Test_ContextDeleteCmd_CredentialName_Failure(t *testing.T) {
tempDir := t.TempDir()
data := Initialize(t, tempDir)
defer ConfigCleanup(t, data)
SetMockKeyring(t)
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{
@ -360,16 +361,16 @@ func Test_ConfigDeleteCmd_CredentialName_Failure(t *testing.T) {
t.Fatal(err)
}
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"config", "delete", "credentials.serveraddress", "--name", "harbor-cli@http://goharbor.io"})
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_ConfigDeleteCmd_Current_Flag_Success(t *testing.T) {
func Test_ContextDeleteCmd_Current_Flag_Success(t *testing.T) {
tempDir := t.TempDir()
data := Initialize(t, tempDir)
defer ConfigCleanup(t, data)
SetMockKeyring(t)
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{
@ -392,7 +393,7 @@ func Test_ConfigDeleteCmd_Current_Flag_Success(t *testing.T) {
t.Fatal(err)
}
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"config", "delete", "--current"})
rootCmd.SetArgs([]string{"context", "delete", "--current"})
err = rootCmd.Execute()
assert.NoError(t, err)
config, err := utils.GetCurrentHarborConfig()
@ -404,13 +405,13 @@ func Test_ConfigDeleteCmd_Current_Flag_Success(t *testing.T) {
assert.NoError(t, err)
}
func Test_ConfigDeleteCmd_Current_Flag_With_Item_Failure(t *testing.T) {
func Test_ContextDeleteCmd_Current_Flag_With_Item_Failure(t *testing.T) {
tempDir := t.TempDir()
data := Initialize(t, tempDir)
defer ConfigCleanup(t, data)
SetMockKeyring(t)
data := helpers.Initialize(t, tempDir)
defer helpers.ConfigCleanup(t, data)
helpers.SetMockKeyring(t)
rootCmd := root.RootCmd()
rootCmd.SetArgs([]string{"config", "delete", "credentials.serveraddress", "--current"})
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

@ -11,7 +11,7 @@
// 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
package context
import (
"fmt"
@ -25,9 +25,9 @@ import (
var deleteCurrent bool
// DeleteConfigItemCommand creates the 'harbor config delete' subcommand,
// allowing you to do: harbor config delete <item>
func DeleteConfigItemCommand() *cobra.Command {
// 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{
@ -35,13 +35,13 @@ func DeleteConfigItemCommand() *cobra.Command {
Short: "Delete (clear) a specific config item",
Example: `
# Clear the current credential's password
harbor config delete credentials.password
harbor context delete credentials.password
# Clear a specific credential's password using --name
harbor config delete credentials.password --name admin@http://demo.goharbor.io
harbor context delete credentials.password --name admin@http://demo.goharbor.io
# Clear the current credential
harbor config delete --current
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.

View File

@ -11,7 +11,7 @@
// 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
package context
import (
"encoding/json"
@ -26,7 +26,7 @@ import (
)
// GetConfigItemCommand creates the 'harbor config get' subcommand.
func GetConfigItemCommand() *cobra.Command {
func GetContextItemCommand() *cobra.Command {
var credentialName string
cmd := &cobra.Command{
@ -34,7 +34,7 @@ func GetConfigItemCommand() *cobra.Command {
Short: "Get a specific config item",
Example: `
# Get the current credential's username
harbor config get credentials.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

View File

@ -11,29 +11,29 @@
// 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
package context
import (
"fmt"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/sirupsen/logrus"
"github.com/goharbor/harbor-cli/pkg/views/context/list"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"gopkg.in/yaml.v3"
)
func ListConfigCommand() *cobra.Command {
func ListContextCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List config items",
Example: ` harbor config list`,
Long: `Get information of all CLI config items`,
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 {
logrus.Errorf("Failed to get config: %v", err)
fmt.Println("failed to get config: ", utils.ParseHarborErrorMsg(err))
return
}
@ -43,19 +43,19 @@ func ListConfigCommand() *cobra.Command {
// Use utils.PrintFormat if available
err = utils.PrintFormat(config, formatFlag)
if err != nil {
logrus.Errorf("Failed to print config: %v", err)
}
} else {
// Default to YAML format
data, err := yaml.Marshal(config)
if err != nil {
logrus.Errorf("Failed to marshal config to YAML: %v", err)
fmt.Println("Failed to print config: ", utils.ParseHarborErrorMsg(err))
return
}
fmt.Println(string(data))
} 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

@ -11,7 +11,7 @@
// 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
package context
import (
"fmt"
@ -26,7 +26,7 @@ import (
// UpdateConfigItemCommand creates the 'harbor config update' subcommand,
// allowing you to do: harbor config update <item> <value>.
func UpdateConfigItemCommand() *cobra.Command {
func UpdateContextItemCommand() *cobra.Command {
var credentialName string
cmd := &cobra.Command{
@ -34,7 +34,7 @@ func UpdateConfigItemCommand() *cobra.Command {
Short: "Set/update a specific config item",
Example: `
# Set/update the current credential's password
harbor config update credentials.password myNewSecret
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

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`,

View File

@ -16,88 +16,73 @@ package root
import (
"fmt"
"github.com/goharbor/go-client/pkg/sdk/v2.0/client/systeminfo"
"github.com/goharbor/go-client/pkg/sdk/v2.0/client/user"
"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: "Show the current credential information",
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 {
currentCredential := viper.GetString("current-credential-name")
if currentCredential == "" {
return fmt.Errorf("no active credentials found")
var cliinfo *api.CLIInfo
var err error
generalInfo, err := api.GetSystemInfo()
if err != nil {
return err
}
var registryAddress string
creds := viper.Get("credentials").([]interface{})
for _, cred := range creds {
c := cred.(map[string]interface{})
if c["name"] == currentCredential {
registryAddress = c["serveraddress"].(string)
break
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)
}
if registryAddress == "" {
return fmt.Errorf("registry address not found for current credential: %s", currentCredential)
}
ctx, client, err := utils.ContextWithClient()
if err != nil {
return fmt.Errorf("failed to create Harbor client: %v", err)
}
userInfo, err := client.User.GetCurrentUserInfo(ctx, &user.GetCurrentUserInfoParams{})
if err != nil {
return fmt.Errorf("failed to get current user info: %v", err)
}
isSysAdmin := userInfo.Payload.SysadminFlag
sysInfo, err := client.Systeminfo.GetSystemInfo(ctx, &systeminfo.GetSystemInfoParams{})
if err != nil {
return fmt.Errorf("failed to get system info: %v", err)
}
harborVersion := sysInfo.Payload.HarborVersion
fmt.Println("\nHarbor CLI Info:")
fmt.Println("==================")
fmt.Printf("Logged in as: %s\n", userInfo.Payload.Username)
fmt.Printf("Registry: %s\n", registryAddress)
fmt.Printf("Harbor Version: %s\n", *harborVersion)
fmt.Printf("Connected as Admin: %s\n", roleString(isSysAdmin))
// Previously logged-in registries
fmt.Println("\nPreviously Logged in to the following registries:")
previousRegistriesMap := make(map[string]struct{})
for _, cred := range creds {
c := cred.(map[string]interface{})
if registry, ok := c["serveraddress"].(string); ok {
previousRegistriesMap[registry] = struct{}{}
}
}
for registry := range previousRegistriesMap {
fmt.Printf("- %s\n", registry)
}
fmt.Printf("\nCLI Version: %s\n", version.Version)
fmt.Printf("OS: %s\n", version.System)
return nil
},
}
return cmd
}
func roleString(isSysAdmin bool) string {
if isSysAdmin {
return "Yes"
}
return "No"
}

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

@ -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

@ -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

@ -78,7 +78,6 @@ func LoginCommand() *cobra.Command {
}
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")
@ -88,22 +87,14 @@ func LoginCommand() *cobra.Command {
// 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 if not provided.
if loginView.Name == "" && loginView.Server != "" && loginView.Username != "" {
loginView.Name = fmt.Sprintf("%s@%s", loginView.Username, utils.SanitizeServerAddress(loginView.Server))
}
// 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 a name is provided, try to load the matching credential from the config.
if loginView.Name != "" {
loadedLoginView, err := LoadCredentialsIntoLoginView(loginView.Name, config)
if err != nil {
return fmt.Errorf("failed to load credentials: %w", err)
}
return RunLogin(loadedLoginView)
}
// If nothing matches, launch the interactive view.
return CreateLoginView(&loginView)
}
@ -124,29 +115,6 @@ func CreateLoginView(loginView *login.LoginView) error {
return RunLogin(*loginView)
}
// LoadCredentialsIntoLoginView loads a stored credential from the config by name and returns a LoginView.
func LoadCredentialsIntoLoginView(credentialName string, config *utils.HarborConfig) (login.LoginView, error) {
for _, cred := range config.Credentials {
if cred.Name == credentialName {
key, err := utils.GetEncryptionKey()
if err != nil {
return login.LoginView{}, fmt.Errorf("failed to get encryption key: %w", err)
}
decryptedPassword, err := utils.Decrypt(key, string(cred.Password))
if err != nil {
return login.LoginView{}, fmt.Errorf("failed to decrypt password: %w", err)
}
return login.LoginView{
Server: cred.ServerAddress,
Username: cred.Username,
Password: decryptedPassword,
Name: cred.Name,
}, nil
}
}
return login.LoginView{}, fmt.Errorf("credential with name %s not found", credentialName)
}
// RunLogin attempts to log in using the provided LoginView credentials.
func RunLogin(opts login.LoginView) error {
opts.Server = utils.FormatUrl(opts.Server)
@ -156,11 +124,15 @@ func RunLogin(opts login.LoginView) error {
Username: opts.Username,
Password: opts.Password,
}
err := utils.ValidateURL(opts.Server)
if err != nil {
return fmt.Errorf("invalid server URL: %s", err)
}
client := utils.GetClientByConfig(clientConfig)
ctx := context.Background()
_, err := client.User.GetCurrentUserInfo(ctx, &user.GetCurrentUserInfoParams{})
_, err = client.User.GetCurrentUserInfo(ctx, &user.GetCurrentUserInfoParams{})
if err != nil {
return fmt.Errorf("login failed, please check your credentials: %s", err)
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)
@ -195,12 +167,14 @@ func RunLogin(opts login.LoginView) error {
if existingCred.Username == opts.Username && existingCred.ServerAddress == opts.Server {
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 {
@ -208,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
}
}
@ -216,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,54 +11,50 @@
// 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)
func Test_Login_Success(t *testing.T) {
tempDir := t.TempDir()
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",
}
// SetMockKeyring(t)
for _, serverAddress := range validServerAddresses {
t.Run("ValidServer_"+serverAddress, func(t *testing.T) {
args := []string{serverAddress}
cmd.SetArgs(args)
// cmd := root.LoginCommand()
// validServerAddresses := []string{
// "http://demo.goharbor.io:80",
// "https://demo.goharbor.io:443",
// "http://demo.goharbor.io",
// "https://demo.goharbor.io",
// }
assert.NoError(t, cmd.Flags().Set("username", "harbor-cli"))
assert.NoError(t, cmd.Flags().Set("password", "Harbor12345"))
// for _, serverAddress := range validServerAddresses {
// t.Run("ValidServer_"+serverAddress, func(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"))
// err := cmd.Execute()
// assert.NoError(t, err, "Expected no error for server: %s", serverAddress)
// })
// }
// }
err := cmd.Execute()
assert.NoError(t, err, "Expected no error for server: %s", serverAddress)
})
}
}
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"))
@ -68,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"))
@ -84,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

@ -15,18 +15,18 @@ package config
import "github.com/spf13/cobra"
func Config() *cobra.Command {
var isID bool
func ProjectConfigCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "config",
Short: "Manage the config of the Harbor Cli",
Long: `Manage repositories in Harbor config`,
Short: "Manage project configuration",
}
cmd.AddCommand(
ListConfigCommand(),
GetConfigItemCommand(),
UpdateConfigItemCommand(),
DeleteConfigItemCommand(),
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,37 +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 == "" {
log.Errorf("Use the --registry-id flag with a registry ID")
} else {
err = api.CreateProject(opts)
}
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
@ -74,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,15 +14,18 @@
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"
)
// DeleteProjectCommand creates a new `harbor project delete` command
// DeleteProjectCommand creates a new `harbor delete project` command
func DeleteProjectCommand() *cobra.Command {
var forceDelete bool
var projectID string
@ -30,59 +33,101 @@ func DeleteProjectCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "delete",
Short: "Delete project by name or ID",
Example: "harbor project delete [projectname] or harbor project delete --project-id [projectid]",
Long: "Delete project by name or ID. If no arguments are provided, it will prompt for the project name. Use --project-id to specify the project ID directly. The --force flag will delete all repositories and artifacts within the project.",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
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
errChan := make(chan error, len(args))
var mu sync.Mutex
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 {
errChan <- err
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(name string) {
go func(projectName string) {
defer wg.Done()
if err := api.DeleteProject(name, forceDelete, false); err != nil {
errChan <- err
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()
}
}(projectName)
}(pn)
}
} else {
projectName := prompt.GetProjectNameFromUser()
// 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 {
log.Errorf("failed to delete project: %v", err)
return fmt.Errorf("failed to delete project: %v", utils.ParseHarborErrorMsg(err))
}
fmt.Printf("Project '%s' deleted successfully\n", projectName)
return nil
}
wg.Wait()
if len(successfulDeletes) > 0 {
fmt.Println("Successfully deleted projects:")
for _, name := range successfulDeletes {
fmt.Printf(" - %s\n", name)
}
}
go func() {
wg.Wait()
close(errChan)
}()
var finalErr error
for err := range errChan {
if finalErr == nil {
finalErr = err
} else {
log.Errorf("Error: %v", err)
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))
}
if finalErr != nil {
log.Errorf("failed to delete some projects: %v", finalErr)
}
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

@ -37,6 +37,8 @@ func ListProjectCommand() *cobra.Command {
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")
}
@ -44,35 +46,43 @@ func ListProjectCommand() *cobra.Command {
if private && public {
return fmt.Errorf("Cannot specify both --private and --public flags")
}
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", err)
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 {
return err
}
} else {
log.Debug("Listing projects using default view")
list.ListProjects(allProjects)
}
return nil
},
}
@ -92,24 +102,37 @@ func ListProjectCommand() *cobra.Command {
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

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,34 +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 projectNameOrID string
var projectName string
var project *project.GetProjectOK
if len(args) > 0 {
projectNameOrID = args[0]
log.Debugf("Project name provided: %s", args[0])
projectName = args[0]
} else {
projectNameOrID = prompt.GetProjectNameFromUser()
isID = false
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(projectNameOrID, isID)
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

@ -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, false)
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, false)
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

@ -46,10 +46,13 @@ 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, false)
repos, err = api.ListRepository(projectName, false, opts)
if err != nil {
return fmt.Errorf("failed to list repositories: %v", err)
}

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
}

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 scanner
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/scanner/metadata"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func MetadataCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "metadata [scanner-name]",
Short: "Retrieve metadata for a specific scanner",
Long: `Retrieve detailed metadata for a specified scanner integration in Harbor.
You can either:
- Provide the scanner name as an argument (recommended), or
- Leave it blank to be prompted interactively.
The metadata includes supported MIME types, capabilities, vendor information, and more.
Examples:
# Get metadata for a specific scanner by name
harbor scanner metadata trivy-scanner
# Interactively select a scanner if no name is provided
harbor scanner metadata
Flags:
--output-format <format> Output format: 'json' or 'yaml' (default is table view)`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) 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()
}
meta, err := api.GetScannerMetadata(registrationID)
if err != nil {
return fmt.Errorf("failed to get scanner metadata: %v", err)
}
formatFlag := viper.GetString("output-format")
if formatFlag != "" {
err = utils.PrintFormat(meta, formatFlag)
if err != nil {
return err
}
} else {
metadata.DisplayScannerMetadata(meta.Payload)
}
return nil
},
}
return cmd
}

View File

@ -0,0 +1,55 @@
// 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"
"github.com/spf13/cobra"
)
func SetDefaultCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "set-default",
Short: "Set the default scanner for Harbor",
Long: `Set the scanner that will be used as the default in Harbor. This scanner will be used for all default scanning tasks unless another scanner is specified.`,
Aliases: []string{"sd"},
Example: `harbor scanner set-default [scanner-name]
OR
harbor scanner set-default --id <scanner-id>`,
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.SetDefaultScanner(registrationID)
if err != nil {
return fmt.Errorf("failed to set default scanner: %v", err)
}
fmt.Printf("Scanner %q successfully set as the default.\n", registrationID)
return nil
},
}
return cmd
}

View File

@ -0,0 +1,131 @@
// 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/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/prompt"
"github.com/goharbor/harbor-cli/pkg/views/scanner/create"
"github.com/goharbor/harbor-cli/pkg/views/scanner/update"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func UpdateCommand() *cobra.Command {
var opts create.CreateView
cmd := &cobra.Command{
Use: "update [scanner-name]",
Short: "Update a scanner registration",
Long: `Update the fields of an existing scanner registration.
You can pass the scanner name as an argument, or the CLI will prompt you to enter a scanner ID.
Only the fields passed through flags will be updated; other fields will retain their existing values.`,
Example: `
# Update description and URL for a scanner named 'trivy-scanner'
harbor scanner update trivy-scanner --description "Updated scanner" --url "http://trivy.local:8080"
# Change the authentication method and credential
harbor scanner update trivy-scanner --auth Basic --credential "base64encodedAuth"
# Disable the scanner and rename it
harbor scanner update trivy-scanner --name "trivy-secure" --disabled
# If no name is passed, you'll be prompted to enter a Scanner ID
harbor scanner update --description "Updated via ID prompt"
`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) 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()
}
resp, err := api.GetScanner(registrationID)
if err != nil {
return fmt.Errorf("scanner not found with ID %q: %v", registrationID, err)
}
existing := resp.GetPayload()
updateView := &models.ScannerRegistration{
Name: existing.Name,
Description: existing.Description,
Auth: existing.Auth,
AccessCredential: existing.AccessCredential,
URL: existing.URL,
Disabled: existing.Disabled,
SkipCertVerify: existing.SkipCertVerify,
UseInternalAddr: existing.UseInternalAddr,
}
flags := cmd.Flags()
if flags.Changed("name") {
updateView.Name = opts.Name
}
if flags.Changed("description") {
updateView.Description = opts.Description
}
if flags.Changed("auth") {
updateView.Auth = opts.Auth
}
if flags.Changed("credential") {
updateView.AccessCredential = opts.AccessCredential
}
if flags.Changed("url") {
updateView.URL = strfmt.URI(opts.URL)
}
if flags.Changed("disabled") {
updateView.Disabled = &opts.Disabled
}
if flags.Changed("skip-cert-verification") {
updateView.SkipCertVerify = &opts.SkipCertVerify
}
if flags.Changed("use-internal-addr") {
updateView.UseInternalAddr = &opts.UseInternalAddr
}
update.UpdateScannerView(updateView)
err = api.UpdateScanner(registrationID, *updateView)
if err != nil {
return fmt.Errorf("failed to update scanner: %v", err)
}
log.Infof("Scanner %q updated successfully", updateView.Name)
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")
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 scanner
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"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/scanner/view"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func ViewCommand() *cobra.Command {
return &cobra.Command{
Use: "view [scanner-name]",
Short: "Display detailed information about a scanner registration",
Long: `Display full details of a scanner registration in Harbor.
You can:
- Provide a scanner name to view its details directly.
- Omit the argument to select a scanner interactively by ID.
Supports custom output formats using the --output-format flag (e.g., json, yaml, table).
Examples:
# View a specific scanner by name
harbor scanner view trivy-scanner
# Interactively choose a scanner to view
harbor scanner view
# View scanner in JSON format
harbor scanner view trivy-scanner --output-format=json`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var scanner *models.ScannerRegistration
if len(args) > 0 {
resp, err := api.GetScannerByName(args[0])
if err != nil {
return fmt.Errorf("failed to get scanner by name %q: %v", args[0], err)
}
scanner = &resp
} else {
id := prompt.GetScannerIdFromUser()
resp, err := api.GetScanner(id)
if err != nil {
return fmt.Errorf("failed to get scanner by ID %q: %v", id, err)
}
scanner = resp.GetPayload()
}
outputFormat := viper.GetString("output-format")
if outputFormat != "" {
if err := utils.PrintFormat(scanner, outputFormat); err != nil {
return fmt.Errorf("failed to format output: %v", err)
}
} else {
view.ViewScanner(scanner)
}
return nil
},
}
}

View File

@ -16,6 +16,7 @@ package immutable
import (
"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/immutable/create"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@ -32,6 +33,7 @@ func CreateImmutableCommand() *cobra.Command {
Example: "harbor tag immutable create",
Run: func(cmd *cobra.Command, args []string) {
var err error
var projectName string
createView := &create.CreateView{
ScopeSelectors: create.ImmutableSelector{
Decoration: opts.ScopeSelectors.Decoration,
@ -43,12 +45,15 @@ func CreateImmutableCommand() *cobra.Command {
},
}
if len(args) > 0 {
err = createImmutableView(createView, args[0])
projectName = args[0]
} else {
projectName := prompt.GetProjectNameFromUser()
err = createImmutableView(createView, projectName)
projectName, err = prompt.GetProjectNameFromUser()
if err != nil {
log.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
}
}
err = createImmutableView(createView, projectName)
if err != nil {
log.Errorf("failed to create immutable tag rule: %v", err)
}

View File

@ -16,6 +16,7 @@ package immutable
import (
"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"
)
@ -33,7 +34,10 @@ func DeleteImmutableCommand() *cobra.Command {
projectName = args[0]
immutableId = prompt.GetImmutableTagRule(args[0])
} else {
projectName = prompt.GetProjectNameFromUser()
projectName, err = prompt.GetProjectNameFromUser()
if err != nil {
log.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
}
immutableId = prompt.GetImmutableTagRule(projectName)
}
err = api.DeleteImmutable(projectName, immutableId)

View File

@ -44,18 +44,23 @@ You can specify the project name as an argument or, if omitted, you will be prom
Run: func(cmd *cobra.Command, args []string) {
var err error
var resp immutable.ListImmuRulesOK
var projectName string
if len(args) > 0 {
projectName := args[0]
resp, err = api.ListImmutable(projectName)
projectName = args[0]
} else {
projectName := prompt.GetProjectNameFromUser()
resp, err = api.ListImmutable(projectName)
projectName, err = prompt.GetProjectNameFromUser()
if err != nil {
log.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
return
}
}
resp, err = api.ListImmutable(projectName)
if err != nil {
log.Errorf("failed to list immutablility rule: %v", err)
}
FormatFlag := viper.GetString("output-format")
if FormatFlag != "" {
utils.PrintPayloadInJSONFormat(resp)

View File

@ -36,6 +36,10 @@ func ElevateUserCmd() *cobra.Command {
log.Errorf("failed to get user id for '%s': %v", args[0], err)
return
}
if userId == 0 {
log.Errorf("User with name '%s' not found", args[0])
return
}
} else {
userId = prompt.GetUserIdFromUser()
}
@ -50,10 +54,12 @@ func ElevateUserCmd() *cobra.Command {
}
err = api.ElevateUser(userId)
if isUnauthorizedError(err) {
log.Error("Permission denied: Admin privileges are required to execute this command.")
} else {
log.Errorf("failed to elevate user: %v", err)
if err != nil {
if isUnauthorizedError(err) {
log.Error("Permission denied: Admin privileges are required to execute this command.")
} else {
log.Errorf("failed to elevate user: %v", err)
}
}
},
}

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