Compare commits

...

46 Commits
v0.0.6 ... 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
294 changed files with 15749 additions and 733 deletions

1
.dagger/.gitignore vendored
View File

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

View File

@ -38,6 +38,20 @@ Runs `golangci-lint` on your code and saves the report to a file.
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.

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,8 +24,7 @@ import (
const (
GOLANGCILINT_VERSION = "v2.1.2"
GO_VERSION = "1.24.2"
SYFT_VERSION = "v1.9.0"
GO_VERSION = "1.24.4"
GORELEASER_VERSION = "v2.8.2"
)
@ -237,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
@ -295,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().
@ -306,11 +302,64 @@ 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().

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

@ -1,28 +1,58 @@
![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/.config/harbor-cli/config.yaml:/root/.config/harbor-cli/config.yaml \
@ -32,7 +62,11 @@ docker run -ti --rm -v $HOME/.config/harbor-cli/config.yaml:/root/.config/harbor
```
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.
# Add the following command to create an alias and append the alias to your .zshrc or .bashrc file
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
```shell
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
@ -45,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:
@ -115,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
@ -190,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
@ -208,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.
@ -238,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,11 +28,14 @@ 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])
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 {
@ -41,8 +46,9 @@ func DeleteArtifactCommand() *cobra.Command {
}
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

@ -51,7 +51,10 @@ Supports pagination, search queries, and sorting using flags.`,
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, err = prompt.GetProjectNameFromUser()
if err != nil {

View File

@ -14,6 +14,8 @@
package artifact
import (
"fmt"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
@ -44,24 +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])
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 {
log.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
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
@ -78,7 +84,10 @@ func StopScanArtifactCommand() *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 {
var projectName string
projectName, err = prompt.GetProjectNameFromUser()

View File

@ -48,23 +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
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)
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)
}
@ -85,13 +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, 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)
}
@ -125,22 +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 {
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)
tag := prompt.GetTagFromUser(repoName, projectName, reference)
err = api.DeleteTag(projectName, repoName, reference, tag)
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

@ -36,7 +36,10 @@ func ViewArtifactCommmand() *cobra.Command {
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, err = prompt.GetProjectNameFromUser()
if err != nil {
@ -55,7 +58,7 @@ func ViewArtifactCommmand() *cobra.Command {
}
}
artifact, err = api.ViewArtifact(projectName, repoName, reference)
artifact, err = api.ViewArtifact(projectName, repoName, reference, false)
if err != nil {
log.Errorf("failed to get info of an artifact: %v", err)

View File

@ -19,14 +19,17 @@ import (
"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"
"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"
@ -93,26 +96,102 @@ harbor help
fmt.Println(err.Error())
}
root.AddCommand(
versionCommand(),
LoginCommand(),
config.Config(),
HealthCommand(),
project.Project(),
registry.Registry(),
repository.Repository(),
user.User(),
artifact.Artifact(),
scanner.Scanner(),
tag.TagCommand(),
cve.CVEAllowlist(),
schedule.Schedule(),
labels.Labels(),
InfoCommand(),
webhook.Webhook(),
instance.Instance(),
quota.Quota(),
)
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

@ -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,19 +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: `The config command allows you to manage configurations of the Harbor CLI.
You can add, get, or delete specific config item, as well as list all config items of the Harbor Cli`,
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

@ -39,15 +39,19 @@ func CreateProjectCommand() *cobra.Command {
}
if opts.ProxyCache && opts.RegistryID == "" {
return fmt.Errorf("Error: Proxy cache selected but no registry ID provided. Use --registry-id.")
return fmt.Errorf("proxy cache selected but no registry ID provided. Use --registry-id")
}
if opts.ProjectName != "" {
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("No project name provided. Switching to interactive view...")
log.Debug("Switching to interactive view...")
createView := &create.CreateView{
ProjectName: opts.ProjectName,
Public: opts.Public,
@ -71,7 +75,7 @@ func CreateProjectCommand() *cobra.Command {
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

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

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

@ -32,7 +32,11 @@ func RepoDeleteCmd() *cobra.Command {
var projectName string
var repoName string
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, err = prompt.GetProjectNameFromUser()
if err != nil {

View File

@ -52,7 +52,7 @@ func ListRepositoryCommand() *cobra.Command {
}
}
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,7 +36,11 @@ 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, err = prompt.GetProjectNameFromUser()
if err != nil {

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

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

View File

@ -1,6 +1,6 @@
{
"name": "harbor-cli",
"engineVersion": "v0.18.4",
"engineVersion": "v0.18.10",
"sdk": {
"source": "go"
},

20
doc/_index.md Normal file
View File

@ -0,0 +1,20 @@
---
title: Harbor CLI Documentation
---
Welcome to the Harbor CLI documentation. This provides detailed documentation for the Harbor CLI.
## Harbor CLI Documentation
This section describes the comprehensive set of commands provided by the Harbor CLI, which enables you to efficiently manage and interact with your Harbor registry.
- `harbor` - Configure the Harbor CLI and set global flags to customize your experience.
- `harbor artifact` - Manage artifacts in Harbor Repository
- `harbor project` - Manage projects and assign resources to them
- `harbor registry` - Manage registries in Harbor
- `harbor repo` - Manage repositories in Harbor context
- `harbor user` - Administer users in Harbor, including creating, updating, and managing user accounts
## Access the Documentation Source Files
The source files for this documentation set are located in the [Harbor CLI repository on Github](https://github.com/goharbor/harbor-cli/tree/main/doc/cli-docs).

110
doc/cli-config/_index.md Normal file
View File

@ -0,0 +1,110 @@
---
title: Harbor CLI Config Management
weight: 25
---
# Harbor CLI Configuration Management
> **Note**
> The Harbor CLI follows the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) for configuration and data storage by default.
## Introduction
Harbor CLI is a flexible command-line tool that lets you manage various Harbor environments with different credentials. Whether you need a production-ready setup or quick testing configurations, the CLI's hierarchical structure and XDG support help keep things organized.
## Understanding the Configuration Structure
The Harbor CLI can manage multiple credentials and keep track of which credential is currently active. This setup allows you to maintain separate contexts for different Harbor instances or user accounts without having to rewrite configuration files manually. While the Harbor CLI configuration file manages your credentials, passwords themselves are never stored in plain text. Instead, they are secured using the AES-GCM encryption described in the [Harbor CLI Encryption documentation](../cli-config).
### Example Configuration File
Below is a simplified example of a typical Harbor CLI configuration file:
```yaml
current-credential-name: example@demo-harbor
credentials:
- name: example@demo-harbor
username: example-user
password: example-password
serveraddress: https://demo.goharbor.io
```
In this configuration:
- **current-credential-name** references the active credential by name.
- **credentials** holds one or more sets of user credentials, each following the same structure.
## Managing Multiple Credentials
If you need to work with multiple sets of credentials—such as development, staging, or production — Harbor CLI makes it easy to create and switch between them.
> **Note**: For more login command details please refer to the [login command reference](../cli-docs/harbor-login.md).
### Creating a New Credentials Entry
Use the `harbor login` command with the required arguments to store new credentials:
```bash
harbor login --name my-new-credential \
--username myuser \
--password mypass \
https://my-harbor-instance.com
```
This adds a new entry to your credentials list, allowing you to manage different Harbor accounts from the same CLI.
### Switching Between Credentials
To switch to another credential set, run:
```bash
harbor login --name <name-of-credential>
```
The CLI will then set the specified credential as the active one, eliminating the need to manually edit your configuration files. This will overwrite the `current-credential-name`.
## Configuration Hierarchy (Highest to Lowest Priority)
1. **Explicit Config Flag**
Provide a custom config file at runtime using `--config`:
```bash
harbor --config /path/to/custom/config.yaml artifact list
```
2. **Environment Variable**
Set a persistent configuration through the `HARBOR_CLI_CONFIG` environment variable:
```bash
export HARBOR_CLI_CONFIG="$HOME/.custom/harbor-config.yaml"
harbor artifact list # Uses the environment-specified config
```
3. **XDG Default Paths**
Automatically discover configuration in the following order:
```bash
${XDG_CONFIG_HOME}/harbor-cli/config.yaml # If XDG_CONFIG_HOME is set
~/.config/harbor-cli/config.yaml # Fallback default
```
## Data Storage Management
### Data File Location
- **Primary Path**: `$XDG_DATA_HOME/harbor-cli/data.yaml`
- **Fallback Path**: `$HOME/.local/share/harbor-cli/data.yaml`
> **Important**
> The data file automatically tracks the last-used configuration file path
## Configuration Precedence Summary
| Priority | Method | Example |
|----------|----------------------------|---------------------------------------|
| 1 | --config flag | harbor --config ./test.yaml ... |
| 2 | HARBOR_CLI_CONFIG env var | export HARBOR_CLI_CONFIG=... |
| 3 | XDG Default Locations | ~/.config/harbor-cli/config.yaml |
## Practical Usage Examples
### Scenario 1: Temporary Config Override
```bash
harbor --config ./experimental.yaml project create "new-project"
```
### Scenario 2: Persistent Environment-based Config
```bash
echo 'export HARBOR_CLI_CONFIG="$HOME/work/configs/prod-harbor.yaml"' >> ~/.zshrc
source ~/.zshrc
harbor config list # Uses production config
```
### Scenario 3: Reset to Default Configuration
```bash
unset HARBOR_CLI_CONFIG
harbor config delete --current # Deletes current context
```

View File

@ -0,0 +1,48 @@
---
title: harbor artifact label add
weight: 95
---
## harbor artifact label add
### Description
##### Attach a label to an artifact in a Harbor project repository
### Synopsis
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
```sh
harbor artifact label add [flags]
```
### Options
```sh
-h, --help help for add
```
### Options inherited from parent commands
```sh
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
-o, --output-format string Output format. One of: json|yaml
-v, --verbose verbose output
```
### SEE ALSO
* [harbor artifact label](harbor-artifact-label.md) - label command for artifacts

View File

@ -0,0 +1,50 @@
---
title: harbor artifact label delete
weight: 90
---
## harbor artifact label delete
### Description
##### Detach a label from an artifact in a Harbor project repository
### Synopsis
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
```sh
harbor artifact label delete [flags]
```
### Options
```sh
-h, --help help for delete
```
### Options inherited from parent commands
```sh
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
-o, --output-format string Output format. One of: json|yaml
-v, --verbose verbose output
```
### SEE ALSO
* [harbor artifact label](harbor-artifact-label.md) - label command for artifacts

View File

@ -0,0 +1,56 @@
---
title: harbor artifact label list
weight: 30
---
## harbor artifact label list
### Description
##### Display labels attached to a specific artifact
### Synopsis
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.
```sh
harbor artifact label list [flags]
```
### Examples
```sh
# 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
```
### Options
```sh
-h, --help help for list
```
### Options inherited from parent commands
```sh
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
-o, --output-format string Output format. One of: json|yaml
-v, --verbose verbose output
```
### SEE ALSO
* [harbor artifact label](harbor-artifact-label.md) - label command for artifacts

View File

@ -0,0 +1,43 @@
---
title: harbor artifact label
weight: 70
---
## harbor artifact label
### Description
##### label command for artifacts
### Synopsis
label command for artifact
### Examples
```sh
harbor artifact label add <project>/<repository>/<reference> <label name>
harbor artifact label del <project>/<repository>/<reference> <label name>
```
### Options
```sh
-h, --help help for label
```
### Options inherited from parent commands
```sh
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
-o, --output-format string Output format. One of: json|yaml
-v, --verbose verbose output
```
### SEE ALSO
* [harbor artifact](harbor-artifact.md) - Manage artifacts
* [harbor artifact label add](harbor-artifact-label-add.md) - Attach a label to an artifact in a Harbor project repository
* [harbor artifact label delete](harbor-artifact-label-delete.md) - Detach a label from an artifact in a Harbor project repository
* [harbor artifact label list](harbor-artifact-label-list.md) - Display labels attached to a specific artifact

View File

@ -36,6 +36,7 @@ Manage artifacts in Harbor Repository
* [harbor](harbor.md) - Official Harbor CLI
* [harbor artifact delete](harbor-artifact-delete.md) - delete an artifact
* [harbor artifact label](harbor-artifact-label.md) - label command for artifacts
* [harbor artifact list](harbor-artifact-list.md) - List container artifacts (images, charts, etc.) in a Harbor repository with metadata
* [harbor artifact scan](harbor-artifact-scan.md) - Scan an artifact
* [harbor artifact tags](harbor-artifact-tags.md) - Manage tags of an artifact

View File

@ -1,36 +0,0 @@
---
title: harbor config
weight: 30
---
## harbor config
### Description
##### Manage the config of the Harbor Cli
### Synopsis
Manage repositories in Harbor config
### Options
```sh
-h, --help help for config
```
### Options inherited from parent commands
```sh
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
-o, --output-format string Output format. One of: json|yaml
-v, --verbose verbose output
```
### SEE ALSO
* [harbor](harbor.md) - Official Harbor CLI
* [harbor config delete](harbor-config-delete.md) - Delete (clear) a specific config item
* [harbor config get](harbor-config-get.md) - Get a specific config item
* [harbor config list](harbor-config-list.md) - List config items
* [harbor config set](harbor-config-set.md) - Set a specific config item

View File

@ -1,8 +1,8 @@
---
title: harbor config delete
title: harbor context delete
weight: 85
---
## harbor config delete
## harbor context delete
### Description
@ -15,7 +15,7 @@ Case-insensitive field lookup, but uses the canonical (Go) field name internally
If you specify --name, that credential (rather than the "current" one) will be used.
```sh
harbor config delete <item> [flags]
harbor context delete <item> [flags]
```
### Examples
@ -23,16 +23,20 @@ harbor config delete <item> [flags]
```sh
# 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 harbor-cli@http://demo.goharbor.io
harbor context delete credentials.password --name admin@http://demo.goharbor.io
# Clear the current credential
harbor context delete --current
```
### Options
```sh
--current Remove current credentials from the config
-h, --help help for delete
-n, --name string Name of the credential to delete fields from (default: the current credential)
```
@ -47,5 +51,5 @@ harbor config delete <item> [flags]
### SEE ALSO
* [harbor config](harbor-config.md) - Manage the config of the Harbor Cli
* [harbor context](harbor-context.md) - Manage locally available contexts

View File

@ -1,8 +1,8 @@
---
title: harbor config get
title: harbor context get
weight: 85
---
## harbor config get
## harbor context get
### Description
@ -14,7 +14,7 @@ Get the value of a specific CLI config item.
If you specify --name, that credential (rather than the "current" one) will be used.
```sh
harbor config get <item> [flags]
harbor context get <item> [flags]
```
### Examples
@ -22,10 +22,10 @@ harbor config get <item> [flags]
```sh
# 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 harbor-cli@http://demo.goharbor.io
harbor config get credentials.username --name admin@http://demo.goharbor.io
```
@ -46,5 +46,5 @@ harbor config get <item> [flags]
### SEE ALSO
* [harbor config](harbor-config.md) - Manage the config of the Harbor Cli
* [harbor context](harbor-context.md) - Manage locally available contexts

View File

@ -1,25 +1,21 @@
---
title: harbor config list
title: harbor context list
weight: 25
---
## harbor config list
## harbor context list
### Description
##### List config items
### Synopsis
Get information of all CLI config items
##### List contexts
```sh
harbor config list [flags]
harbor context list [flags]
```
### Examples
```sh
harbor config list
harbor context list
```
### Options
@ -38,5 +34,5 @@ harbor config list [flags]
### SEE ALSO
* [harbor config](harbor-config.md) - Manage the config of the Harbor Cli
* [harbor context](harbor-context.md) - Manage locally available contexts

View File

@ -0,0 +1,38 @@
---
title: harbor context switch
weight: 50
---
## harbor context switch
### Description
##### Switch to a new context
```sh
harbor context switch <none|context> [flags]
```
### Examples
```sh
harbor context switch harbor-cli@https-demo-goharbor-io
```
### Options
```sh
-h, --help help for switch
```
### Options inherited from parent commands
```sh
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
-o, --output-format string Output format. One of: json|yaml
-v, --verbose verbose output
```
### SEE ALSO
* [harbor context](harbor-context.md) - Manage locally available contexts

View File

@ -1,8 +1,8 @@
---
title: harbor config update
title: harbor context update
weight: 10
---
## harbor config update
## harbor context update
### Description
@ -15,7 +15,7 @@ Case-insensitive field lookup, but uses the canonical (Go) field name internally
If you specify --name, that credential (rather than the "current" one) will be updated.
```sh
harbor config update <item> <value> [flags]
harbor context update <item> <value> [flags]
```
### Examples
@ -23,7 +23,7 @@ harbor config update <item> <value> [flags]
```sh
# 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
@ -47,5 +47,5 @@ harbor config update <item> <value> [flags]
### SEE ALSO
* [harbor config](harbor-config.md) - Manage the config of the Harbor Cli
* [harbor context](harbor-context.md) - Manage locally available contexts

View File

@ -0,0 +1,44 @@
---
title: harbor context
weight: 30
---
## harbor context
### Description
##### Manage locally available contexts
### Synopsis
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.
### Examples
```sh
harbor context list
```
### Options
```sh
-h, --help help for context
```
### Options inherited from parent commands
```sh
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
-o, --output-format string Output format. One of: json|yaml
-v, --verbose verbose output
```
### SEE ALSO
* [harbor](harbor.md) - Official Harbor CLI
* [harbor context delete](harbor-context-delete.md) - Delete (clear) a specific config item
* [harbor context get](harbor-context-get.md) - Get a specific config item
* [harbor context list](harbor-context-list.md) - List contexts
* [harbor context switch](harbor-context-switch.md) - Switch to a new context
* [harbor context update](harbor-context-update.md) - Set/update a specific config item

View File

@ -10,7 +10,7 @@ weight: 60
### Synopsis
Create allowlists of CVEs to ignore during vulnerability scanning
Create allowlist of CVEs to ignore during vulnerability scanning
```sh
harbor cve-allowlist add [flags]

View File

@ -6,7 +6,7 @@ weight: 60
### Description
##### list system level allowlist of cve
##### List system level allowlist of cve
```sh
harbor cve-allowlist list [flags]

View File

@ -36,5 +36,5 @@ harbor cve-allowlist list
* [harbor](harbor.md) - Official Harbor CLI
* [harbor cve-allowlist add](harbor-cve-allowlist-add.md) - Add cve allowlist
* [harbor cve-allowlist list](harbor-cve-allowlist-list.md) - list system level allowlist of cve
* [harbor cve-allowlist list](harbor-cve-allowlist-list.md) - List system level allowlist of cve

View File

@ -27,7 +27,7 @@ harbor health [flags]
### Options inherited from parent commands
```sh
--config string config file (default is $HOME/.harbor/config.yaml) (default "/Users/vadim/.harbor/config.yaml")
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
-o, --output-format string Output format. One of: json|yaml
-v, --verbose verbose output
```

View File

@ -1,6 +1,6 @@
---
title: harbor instance create
weight: 0
weight: 10
---
## harbor instance create

View File

@ -27,7 +27,8 @@ harbor instance delete [flags]
### Options
```sh
-h, --help help for delete
-h, --help help for delete
-i, --id int ID of the instance to delete (default -1)
```
### Options inherited from parent commands

View File

@ -1,6 +1,6 @@
---
title: harbor label delete
weight: 0
weight: 30
---
## harbor label delete

View File

@ -20,15 +20,15 @@ harbor login [server] [flags]
```sh
-h, --help help for login
--name string name for the set of credentials
-p, --password string Password
--password-stdin Take the password from stdin
-u, --username string Username
```
### Options inherited from parent commands
```sh
--config string config file (default is $HOME/.harbor/config.yaml) (default "/home/user/.harbor/config.yaml")
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
-o, --output-format string Output format. One of: json|yaml
-v, --verbose verbose output
```

View File

@ -0,0 +1,49 @@
---
title: harbor logs
weight: 30
---
## harbor logs
### Description
##### Get recent logs of the projects which the user is a member of
### Synopsis
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
```sh
harbor logs [flags]
```
### Options
```sh
-f, --follow Follow log output (tail -f behavior)
-h, --help help for logs
--page int Page number (default 1)
--page-size int Size of per page (default 10)
-q, --query string Query string to query resources
-n, --refresh-interval string Interval to refresh logs when following (default: 5s)
--sort string Sort the resource list in ascending or descending order
```
### Options inherited from parent commands
```sh
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
-o, --output-format string Output format. One of: json|yaml
-v, --verbose verbose output
```
### SEE ALSO
* [harbor](harbor.md) - Official Harbor CLI

View File

@ -0,0 +1,56 @@
---
title: harbor project config list
weight: 85
---
## harbor project config list
### Description
##### List configuration of a Harbor project by name or ID
### Synopsis
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
```sh
harbor project config list [project_name] [flags]
```
### Options
```sh
-h, --help help for list
```
### Options inherited from parent commands
```sh
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
--id Use project ID instead of name
-o, --output-format string Output format. One of: json|yaml
-v, --verbose verbose output
```
### SEE ALSO
* [harbor project config](harbor-project-config.md) - Manage project configuration

View File

@ -0,0 +1,63 @@
---
title: harbor project config update
weight: 10
---
## harbor project config update
### Description
##### Interactively or via flags update project configuration in Harbor
### Synopsis
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"
```sh
harbor project config update [project_name] [flags]
```
### Options
```sh
--auto-scan string Enable or disable auto scan (true/false)
--enable-content-trust string Enable or disable content trust (true/false)
--enable-content-trust-cosign string Enable or disable content trust cosign (true/false)
-h, --help help for update
--prevent-vul string Enable or disable vulnerability prevention (true/false)
--public string Set project visibility (true/false)
--reuse-sys-cve string Enable or disable reuse of system CVE allowlist (true/false)
--severity string Set severity level
```
### Options inherited from parent commands
```sh
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
--id Use project ID instead of name
-o, --output-format string Output format. One of: json|yaml
-v, --verbose verbose output
```
### SEE ALSO
* [harbor project config](harbor-project-config.md) - Manage project configuration

View File

@ -0,0 +1,31 @@
---
title: harbor project config
weight: 45
---
## harbor project config
### Description
##### Manage project configuration
### Options
```sh
-h, --help help for config
--id Use project ID instead of name
```
### Options inherited from parent commands
```sh
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
-o, --output-format string Output format. One of: json|yaml
-v, --verbose verbose output
```
### SEE ALSO
* [harbor project](harbor-project.md) - Manage projects and assign resources to them
* [harbor project config list](harbor-project-config-list.md) - List configuration of a Harbor project by name or ID
* [harbor project config update](harbor-project-config-update.md) - Interactively or via flags update project configuration in Harbor

View File

@ -9,24 +9,23 @@ weight: 80
##### create project
```sh
harbor project create [flags]
harbor project create [project name] [flags]
```
### Options
```sh
-h, --help help for create
--name string Name of the project
--proxy-cache Whether the project is a proxy cache project
--public Project is public or private (default true)
--public Project is public or private
--registry-id string ID of referenced registry when creating the proxy cache project
--storage-limit string Storage quota of the project (default "-1")
--storage-limit string Storage quota of the project
```
### Options inherited from parent commands
```sh
--config string config file (default is $HOME/.harbor/config.yaml) (default "/home/user/.harbor/config.yaml")
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
-o, --output-format string Output format. One of: json|yaml
-v, --verbose verbose output
```

View File

@ -6,22 +6,34 @@ weight: 100
### Description
##### delete project by name or id
##### Delete project by name or ID
### Synopsis
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.
```sh
harbor project delete [flags]
```
### Examples
```sh
harbor project delete [projectname1] [projectname2] or harbor project delete --project-id [projectid]
```
### Options
```sh
-h, --help help for delete
--force Forcefully delete all repositories, artifacts, and policies in the project. Use with extreme caution—this action is irreversible.
-h, --help help for delete
--project-id string Specify project ID instead of project name
```
### Options inherited from parent commands
```sh
--config string config file (default is $HOME/.harbor/config.yaml) (default "/home/user/.harbor/config.yaml")
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
-o, --output-format string Output format. One of: json|yaml
-v, --verbose verbose output
```

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