Compare commits
76 Commits
Author | SHA1 | Date |
---|---|---|
|
ff1485171c | |
|
e15bde4ba0 | |
|
3c8e6239a0 | |
|
6caed4d4f4 | |
|
18cb2da313 | |
|
85618bdd6b | |
|
0fbd8ecdea | |
|
c705221645 | |
|
cc2113c2dd | |
|
3304c2ea0f | |
|
fd38240593 | |
|
97fc4d6c64 | |
|
598b817a32 | |
|
95edbd6f8b | |
|
af41e84a14 | |
|
81ab6301fe | |
|
7805ccdb2a | |
|
8fe88a3fb3 | |
|
c26c926fe3 | |
|
8aa4a2ca84 | |
|
2fd435940d | |
|
c92eb8f687 | |
|
9929fd959a | |
|
1fbe629fef | |
|
700c075be6 | |
|
ba767105b0 | |
|
ce4b52f41e | |
|
60ad0bda48 | |
|
6caf59e1af | |
|
ae0c3df5ce | |
|
87b57b0760 | |
|
7d68faf5a1 | |
|
a1df056b6f | |
|
a813bcfa6a | |
|
968a772377 | |
|
2d678109a0 | |
|
89d1f402e7 | |
|
f492136dee | |
|
415e62a344 | |
|
2a06d9e1d8 | |
|
4accd4da76 | |
|
62a20f0ddb | |
|
ff1425cc7f | |
|
5174b7a6ec | |
|
ffb9af0929 | |
|
f40dbe3022 | |
|
254ed5758c | |
|
4f4ca29a26 | |
|
fd0a626e61 | |
|
4ace830f36 | |
|
be21559a27 | |
|
99b1020d8a | |
|
9265b36353 | |
|
2a84670e2f | |
|
d8d1289ddc | |
|
97c20c0209 | |
|
7fa59cdeff | |
|
1435a29efb | |
|
493665d4d8 | |
|
a12c359b55 | |
|
2258d255b4 | |
|
e7432058e8 | |
|
93e38022ef | |
|
8ae04ff88f | |
|
50a8e01af6 | |
|
52a7b6cbc6 | |
|
629e837f44 | |
|
010cabc252 | |
|
bdb6d985cb | |
|
f9c570862a | |
|
9055c510d7 | |
|
18956214f4 | |
|
45d3b5ac85 | |
|
4c2071f0d0 | |
|
ebf8393b08 | |
|
ffc9a8e18f |
|
@ -2,3 +2,4 @@
|
|||
/internal/dagger
|
||||
/internal/querybuilder
|
||||
/internal/telemetry
|
||||
/.env
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
# 🛠️ Harbor CLI Dagger Pipeline
|
||||
|
||||
We use [Dagger](https://dagger.io) to define a CI/CD pipeline for building, linting, and publishing the [Harbor CLI](https://github.com/goharbor/harbor-cli).
|
||||
This README will help beginners understand how to use Dagger in local development and CI workflows.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you start, ensure you have the following:
|
||||
|
||||
1. Dagger: Install the latest version of Dagger. You can check the official documentation for installation steps: [Dagger Installation Guide](https://docs.dagger.io/install).
|
||||
|
||||
## Dagger Setup and Development Mode
|
||||
|
||||
### Run Dagger Develop
|
||||
|
||||
```bash
|
||||
dagger develop
|
||||
```
|
||||
|
||||
This command will generate the necessary files and configuration for building and running Dagger.
|
||||
|
||||
|
||||
## 📦 Dagger Functions Explained
|
||||
|
||||
### 🔧 `BuildDev(platform)`
|
||||
|
||||
Builds a development binary for your target platform.
|
||||
|
||||
```bash
|
||||
dagger call build-dev --platform="linux/amd64" export --path=bin/harbor-dev
|
||||
```
|
||||
|
||||
### 🧼 `LintReport()`
|
||||
|
||||
Runs `golangci-lint` on your code and saves the report to a file.
|
||||
|
||||
```bash
|
||||
dagger call lint-report export --path=./LintReport.json
|
||||
```
|
||||
|
||||
### 📝 `TestCoverageReport()`
|
||||
|
||||
Runs go test coverage tools and creates a report.
|
||||
```bash
|
||||
dagger call test-coverage-report export --path=coverage-report.md
|
||||
```
|
||||
|
||||
### ✅ `CheckCoverageThreshold(context, threshold)`
|
||||
|
||||
Runs go test coverage tools and creates a report. The total coverage is compared to a threshold that can be set to e.g. 80%.
|
||||
```bash
|
||||
dagger call check-coverage-threshold --threshold 80.0
|
||||
```
|
||||
|
||||
### 🚀 `PublishImage(registry, imageTags)`
|
||||
|
||||
Builds and publishes the Harbor CLI image to the given container registry with proper OCI metadata labels.
|
||||
|
||||
Before running the command you have to export you registry password
|
||||
|
||||
```shell
|
||||
export REGPASS=Harbor12345
|
||||
```
|
||||
|
||||
```bash
|
||||
dagger call publish-image \
|
||||
--registry=demo.goharbor.io \
|
||||
--registry-username=harbor-cli \
|
||||
--registry-password=env:REGPASS \
|
||||
--imageTags=v0.1.0,latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Configuration Constants
|
||||
|
||||
Dagger uses these constant versions (you can modify them as needed):
|
||||
|
||||
```go
|
||||
const (
|
||||
GO_VERSION = "1.24.2"
|
||||
GOLANGCILINT_VERSION = "v2.1.2"
|
||||
SYFT_VERSION = "v1.9.0"
|
||||
GORELEASER_VERSION = "v2.3.2"
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 Tips for Beginners
|
||||
|
||||
- Every container step is **reproducible** you can build locally or in GitHub Actions without changes.
|
||||
- Use Dagger to cache Go builds and lint output, speeding up re-runs.
|
||||
|
||||
---
|
||||
|
||||
## 📚 References
|
||||
|
||||
- [Dagger Go SDK Docs](https://pkg.go.dev/dagger.io/dagger)
|
||||
- [golangci-lint](https://golangci-lint.run/)
|
||||
- [Goreleaser](https://goreleaser.com/)
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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=
|
||||
|
|
118
.dagger/main.go
118
.dagger/main.go
|
@ -24,9 +24,8 @@ import (
|
|||
|
||||
const (
|
||||
GOLANGCILINT_VERSION = "v2.1.2"
|
||||
GO_VERSION = "1.24.2"
|
||||
SYFT_VERSION = "v1.9.0"
|
||||
GORELEASER_VERSION = "v2.3.2"
|
||||
GO_VERSION = "1.24.4"
|
||||
GORELEASER_VERSION = "v2.8.2"
|
||||
)
|
||||
|
||||
func New(
|
||||
|
@ -181,6 +180,9 @@ func (m *HarborCli) PublishImage(
|
|||
}
|
||||
fmt.Printf("provided tags: %s\n", imageTags)
|
||||
|
||||
// Get current time for image creation timestamp
|
||||
creationTime := time.Now().UTC().Format(time.RFC3339)
|
||||
|
||||
for _, builder := range builders {
|
||||
os, _ := builder.EnvVariable(ctx, "GOOS")
|
||||
arch, _ := builder.EnvVariable(ctx, "GOARCH")
|
||||
|
@ -195,6 +197,13 @@ func (m *HarborCli) PublishImage(
|
|||
WithFile("/harbor", builder.File("./harbor")).
|
||||
WithExec([]string{"ls", "-al"}).
|
||||
WithExec([]string{"./harbor", "version"}).
|
||||
// Add required metadata labels for ArtifactHub
|
||||
WithLabel("org.opencontainers.image.created", creationTime).
|
||||
WithLabel("org.opencontainers.image.description", "Harbor CLI - A command-line interface for CNCF Harbor, the cloud native registry!").
|
||||
WithLabel("io.artifacthub.package.readme-url", "https://raw.githubusercontent.com/goharbor/harbor-cli/main/README.md").
|
||||
WithLabel("org.opencontainers.image.source", "https://github.com/goharbor/harbor-cli").
|
||||
WithLabel("org.opencontainers.image.version", version).
|
||||
WithLabel("io.artifacthub.package.license", "Apache-2.0").
|
||||
WithEntrypoint([]string{"/harbor"})
|
||||
releaseImages = append(releaseImages, ctr)
|
||||
}
|
||||
|
@ -218,7 +227,7 @@ func (m *HarborCli) PublishImage(
|
|||
// SnapshotRelease Create snapshot non OCI artifacts with goreleaser
|
||||
func (m *HarborCli) SnapshotRelease(ctx context.Context) *dagger.Directory {
|
||||
return m.goreleaserContainer().
|
||||
WithExec([]string{"goreleaser", "release", "--snapshot", "--clean", "--skip", "validate"}).
|
||||
WithExec([]string{"goreleaser", "release", "--snapshot", "--clean"}).
|
||||
Directory("/src/dist")
|
||||
}
|
||||
|
||||
|
@ -227,32 +236,28 @@ func (m *HarborCli) Release(ctx context.Context, githubToken *dagger.Secret) {
|
|||
goreleaser := m.goreleaserContainer().
|
||||
WithSecretVariable("GITHUB_TOKEN", githubToken).
|
||||
WithExec([]string{"goreleaser", "release", "--clean"})
|
||||
_, err := goreleaser.Stderr(ctx)
|
||||
error, err := goreleaser.Stderr(ctx)
|
||||
if err != nil {
|
||||
log.Printf("Error occured during release: %s", err)
|
||||
return
|
||||
}
|
||||
if len(error) > 0 {
|
||||
log.Printf("Error occured while release: %s", err)
|
||||
return
|
||||
}
|
||||
log.Println("Release tasks completed successfully 🎉")
|
||||
}
|
||||
|
||||
// Return a container with the goreleaser binary mounted and the source directory mounted.
|
||||
func (m *HarborCli) goreleaserContainer() *dagger.Container {
|
||||
// Export the syft binary from the syft container as a file to generate SBOM
|
||||
syft := dag.Container().
|
||||
From(fmt.Sprintf("anchore/syft:%s", SYFT_VERSION)).
|
||||
WithMountedCache("/go/pkg/mod", dag.CacheVolume("syft-gomod")).
|
||||
File("/syft")
|
||||
|
||||
return dag.Container().
|
||||
From(fmt.Sprintf("goreleaser/goreleaser:%s", GORELEASER_VERSION)).
|
||||
WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+GO_VERSION)).
|
||||
WithEnvVariable("GOMODCACHE", "/go/pkg/mod").
|
||||
WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+GO_VERSION)).
|
||||
WithEnvVariable("GOCACHE", "/go/build-cache").
|
||||
WithFile("/bin/syft", syft).
|
||||
WithMountedDirectory("/src", m.Source).
|
||||
WithWorkdir("/src").
|
||||
WithEnvVariable("TINI_SUBREAPER", "true")
|
||||
WithWorkdir("/src")
|
||||
}
|
||||
|
||||
// Generate CLI Documentation and return the directory containing the generated files
|
||||
|
@ -285,6 +290,7 @@ func (m *HarborCli) Test(ctx context.Context) (string, error) {
|
|||
}
|
||||
|
||||
// Executes Go tests and returns TestReport in json file
|
||||
// TestReport executes Go tests and returns only the JSON report file
|
||||
func (m *HarborCli) TestReport(ctx context.Context) *dagger.File {
|
||||
reportName := "TestReport.json"
|
||||
test := dag.Container().
|
||||
|
@ -296,11 +302,93 @@ func (m *HarborCli) TestReport(ctx context.Context) *dagger.File {
|
|||
WithExec([]string{"go", "install", "gotest.tools/gotestsum@latest"}).
|
||||
WithMountedDirectory("/src", m.Source).
|
||||
WithWorkdir("/src").
|
||||
WithExec([]string{"gotestsum", "--jsonfile", reportName})
|
||||
WithExec([]string{"gotestsum", "--jsonfile", reportName, "./..."})
|
||||
|
||||
return test.File(reportName)
|
||||
}
|
||||
|
||||
func (m *HarborCli) TestCoverage(ctx context.Context) *dagger.File {
|
||||
coverage := "coverage.out"
|
||||
test := dag.Container().
|
||||
From("golang:"+GO_VERSION+"-alpine").
|
||||
WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+GO_VERSION)).
|
||||
WithEnvVariable("GOMODCACHE", "/go/pkg/mod").
|
||||
WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+GO_VERSION)).
|
||||
WithEnvVariable("GOCACHE", "/go/build-cache").
|
||||
WithExec([]string{"go", "install", "gotest.tools/gotestsum@latest"}).
|
||||
WithMountedDirectory("/src", m.Source).
|
||||
WithWorkdir("/src").
|
||||
WithExec([]string{"gotestsum", "--", "-coverprofile=" + coverage, "./..."})
|
||||
|
||||
return test.File(coverage)
|
||||
}
|
||||
|
||||
// TestCoverageReport processes coverage data and returns a formatted markdown report
|
||||
func (m *HarborCli) TestCoverageReport(ctx context.Context) *dagger.File {
|
||||
coverageFile := "coverage.out"
|
||||
reportFile := "coverage-report.md"
|
||||
test := dag.Container().
|
||||
From("golang:"+GO_VERSION+"-alpine").
|
||||
WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+GO_VERSION)).
|
||||
WithEnvVariable("GOMODCACHE", "/go/pkg/mod").
|
||||
WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+GO_VERSION)).
|
||||
WithEnvVariable("GOCACHE", "/go/build-cache").
|
||||
WithMountedDirectory("/src", m.Source).
|
||||
WithWorkdir("/src").
|
||||
WithExec([]string{"apk", "add", "--no-cache", "bc"}).
|
||||
WithExec([]string{"go", "test", "-coverprofile=" + coverageFile, "./..."})
|
||||
return test.WithExec([]string{"sh", "-c", `
|
||||
echo "<h2> 📊 Test Coverage Results</h2>" > ` + reportFile + `
|
||||
if [ ! -f "` + coverageFile + `" ]; then
|
||||
echo "<p>❌ Coverage file not found!</p>" >> ` + reportFile + `
|
||||
exit 1
|
||||
fi
|
||||
total_coverage=$(go tool cover -func=` + coverageFile + ` | grep total: | grep -Eo '[0-9]+\.[0-9]+')
|
||||
echo "DEBUG: Total coverage is $total_coverage" >&2
|
||||
if (( $(echo "$total_coverage >= 80.0" | bc -l) )); then
|
||||
emoji="✅"
|
||||
elif (( $(echo "$total_coverage >= 60.0" | bc -l) )); then
|
||||
emoji="⚠️"
|
||||
else
|
||||
emoji="❌"
|
||||
fi
|
||||
echo "<p><b>Total coverage: $emoji $total_coverage% (Target: 80%)</b></p>" >> ` + reportFile + `
|
||||
echo "<details><summary>Detailed package coverage</summary><pre>" >> ` + reportFile + `
|
||||
go tool cover -func=` + coverageFile + ` >> ` + reportFile + `
|
||||
echo "</pre></details>" >> ` + reportFile + `
|
||||
cat ` + reportFile + ` >&2
|
||||
`}).File(reportFile)
|
||||
}
|
||||
|
||||
// Checks for vulnerabilities using govulncheck
|
||||
func (m *HarborCli) vulnerabilityCheck(ctx context.Context) *dagger.Container {
|
||||
return dag.Container().
|
||||
From("golang:"+GO_VERSION+"-alpine").
|
||||
WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+GO_VERSION)).
|
||||
WithEnvVariable("GOMODCACHE", "/go/pkg/mod").
|
||||
WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+GO_VERSION)).
|
||||
WithEnvVariable("GOCACHE", "/go/build-cache").
|
||||
WithExec([]string{"go", "install", "golang.org/x/vuln/cmd/govulncheck@latest"}).
|
||||
WithMountedDirectory("/src", m.Source).
|
||||
WithWorkdir("/src")
|
||||
}
|
||||
|
||||
// Runs a vulnerability check using govulncheck
|
||||
func (m *HarborCli) VulnerabilityCheck(ctx context.Context) (string, error) {
|
||||
return m.vulnerabilityCheck(ctx).
|
||||
WithExec([]string{"govulncheck", "-show", "verbose", "./..."}).
|
||||
Stderr(ctx)
|
||||
}
|
||||
|
||||
// Runs a vulnerability check using govulncheck and writes results to vulnerability-check.report
|
||||
func (m *HarborCli) VulnerabilityCheckReport(ctx context.Context) *dagger.File {
|
||||
report := "vulnerability-check.report"
|
||||
return m.vulnerabilityCheck(ctx).
|
||||
WithExec([]string{
|
||||
"sh", "-c", fmt.Sprintf("govulncheck ./... > %s", report),
|
||||
}).File(report)
|
||||
}
|
||||
|
||||
// Parse the platform string into os and arch
|
||||
func parsePlatform(platform string) (string, string, error) {
|
||||
parts := strings.Split(platform, "/")
|
||||
|
|
|
@ -31,17 +31,17 @@ jobs:
|
|||
|
||||
- name: Check for changes
|
||||
run: |
|
||||
# Check if any newly added docs exist
|
||||
untracked_files=$(git ls-files --others --exclude-standard)
|
||||
# Check if any docs have been modified
|
||||
changed_files=$(git ls-files --others --modified --deleted --exclude-standard)
|
||||
|
||||
# If there are untracked files, fail the workflow
|
||||
if [ -n "$untracked_files" ]; then
|
||||
echo "New Untracked files found"
|
||||
echo "please check if docs were added for new commands"
|
||||
echo "$untracked_files"
|
||||
# If there are files changed, fail the workflow
|
||||
if [ -n "$changed_files" ]; then
|
||||
echo "file changes found"
|
||||
echo "please check if docs were added for new commands or updated for new commands"
|
||||
echo "$changed_files"
|
||||
exit 1 # This will fail the workflow
|
||||
else
|
||||
echo "No untracked files found."
|
||||
echo "No file changes found."
|
||||
fi
|
||||
continue-on-error: false
|
||||
|
||||
|
@ -70,6 +70,53 @@ jobs:
|
|||
# run: |
|
||||
# reviewdog -f=sarif -name="Golang Linter Report" -reporter=github-check -filter-mode nofilter -fail-level any -tee < golangci-lint-report.sarif
|
||||
|
||||
vulnerability-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Dagger Version
|
||||
uses: sagikazarmark/dagger-version-action@v0.0.1
|
||||
|
||||
- name: Run Vulnerability Check
|
||||
uses: dagger/dagger-for-github@v7
|
||||
with:
|
||||
version: ${{ steps.dagger_version.outputs.version }}
|
||||
verb: call
|
||||
args: vulnerability-check-report export --path=vulnerability-check.report
|
||||
|
||||
- name: Generate vulnerability summary
|
||||
run: |
|
||||
echo "<h2> 🔒 Vulnerability Check Results</h2>" >> $GITHUB_STEP_SUMMARY
|
||||
cat vulnerability-check.report >> $GITHUB_STEP_SUMMARY
|
||||
# Check if the lint report contains any content (error or issues)
|
||||
if ! grep -q "No vulnerabilities found." vulnerability-check.report; then
|
||||
# If the file contains content, output an error message and exit with code 1
|
||||
echo "⚠️ Linting issues found!" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
|
||||
test-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Dagger Version
|
||||
uses: sagikazarmark/dagger-version-action@v0.0.1
|
||||
|
||||
- name: Test Release
|
||||
uses: dagger/dagger-for-github@v7
|
||||
with:
|
||||
version: ${{ steps.dagger_version.outputs.version }}
|
||||
verb: call
|
||||
args: snapshot-release
|
||||
|
||||
test-code:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
@ -90,6 +137,33 @@ jobs:
|
|||
with:
|
||||
fromJSONFile: TestReport.json
|
||||
|
||||
- name: Run Test Coverage Report
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: dagger/dagger-for-github@v7
|
||||
with:
|
||||
version: ${{ steps.dagger_version.outputs.version }}
|
||||
verb: call
|
||||
args: test-coverage-report export --path=coverage-report.md
|
||||
|
||||
- name: Add coverage to step summary
|
||||
if: github.event_name == 'pull_request'
|
||||
run: cat coverage-report.md >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Run Test Coverage
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: dagger/dagger-for-github@v7
|
||||
with:
|
||||
version: ${{ steps.dagger_version.outputs.version }}
|
||||
verb: call
|
||||
args: test-coverage export --path=coverage.out
|
||||
|
||||
- uses: codecov/codecov-action@v5
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
verbose: true
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- name: Build Binary
|
||||
uses: dagger/dagger-for-github@v7
|
||||
with:
|
||||
|
|
|
@ -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:
|
||||
|
|
141
CONTRIBUTING.md
141
CONTRIBUTING.md
|
@ -0,0 +1,141 @@
|
|||
# Contributing to Harbor CLI
|
||||
|
||||
Thank you for your interest in contributing to the Harbor CLI project!
|
||||
We welcome contributions of all kinds, from bug fixes and documentation improvements to new features and suggestions.
|
||||
|
||||
## Overview
|
||||
|
||||
The **Harbor CLI** is a powerful command-line tool to interact with the [Harbor container registry](https://goharbor.io/). It's built in Go and helps users manage Harbor resources like projects, registries, artifacts, and more — directly from their terminal.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Run using Container
|
||||
|
||||
You can try the CLI immediately using Docker:
|
||||
|
||||
```bash
|
||||
docker run -ti --rm -v $HOME/.harbor/config.yaml:/root/.harbor/config.yaml registry.goharbor.io/harbor-cli/harbor-cli --help
|
||||
```
|
||||
|
||||
### Alias (Optional)
|
||||
|
||||
```bash
|
||||
echo "alias harbor='docker run -ti --rm -v \$HOME/.harbor/config.yaml:/root/.harbor/config.yaml registry.goharbor.io/harbor-cli/harbor-cli'" >> ~/.zshrc
|
||||
source ~/.zshrc
|
||||
```
|
||||
|
||||
### Build from Source
|
||||
|
||||
Make sure [Go](https://go.dev/) is installed (≥ v1.20).
|
||||
|
||||
```bash
|
||||
git clone https://github.com/goharbor/harbor-cli.git && cd harbor-cli
|
||||
go build -o harbor-cli cmd/harbor/main.go
|
||||
./harbor-cli --help
|
||||
```
|
||||
|
||||
Alternatively, use [Dagger](https://docs.dagger.io/) for isolated builds:
|
||||
|
||||
```bash
|
||||
dagger call build-dev --platform darwin/arm64 export --path=./harbor-cli
|
||||
./harbor-dev --help
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
..
|
||||
├── cmd/harbor/ # Entry point (main.go) and all CLI commands (Cobra-based)
|
||||
├── pkg/ # Shared utilities and internal packages used across commands
|
||||
├── docs/ # Project documentation
|
||||
├── test/ # CLI tests and test data
|
||||
├── .github/ # GitHub workflows and issue templates
|
||||
├── go.mod / go.sum # Go module dependencies
|
||||
└── README.md # Project overview and usage
|
||||
```
|
||||
|
||||
## How to Contribute
|
||||
|
||||
### 1. [Fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) and Clone
|
||||
|
||||
```bash
|
||||
git clone https://github.com/your-username/harbor-cli.git
|
||||
cd harbor-cli
|
||||
```
|
||||
|
||||
### 2. Create Your Feature Branch
|
||||
|
||||
```bash
|
||||
git checkout -b feat/<your-feature-name>
|
||||
```
|
||||
|
||||
### 3. Make Your Changes
|
||||
|
||||
Follow coding and formatting guidelines.
|
||||
|
||||
### 4. Test Locally
|
||||
|
||||
Ensure your changes work as expected.
|
||||
|
||||
```bash
|
||||
gofmt -s -w .
|
||||
dagger call build-dev --platform darwin/arm64 export --path=./harbor-cli #Recommended
|
||||
./harbor-dev --help
|
||||
```
|
||||
|
||||
If dagger is not installed in your system, you can also build the project using the following commands:
|
||||
|
||||
```bash
|
||||
gofmt -s -w .
|
||||
go build -o ./bin/harbor-cli cmd/harbor/main.go
|
||||
./bin/harbor-cli --help
|
||||
```
|
||||
|
||||
### 5. Commit with a clear message
|
||||
|
||||
```bash
|
||||
git commit -s -m "feat(project): add delete command for project resources"
|
||||
```
|
||||
|
||||
### 6. Push and Open a PR
|
||||
|
||||
```bash
|
||||
git push origin feat/<your-feature-name>
|
||||
```
|
||||
|
||||
Then, [Open a Pull Request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request) on GitHub
|
||||
|
||||
## 🧪 Running Tests
|
||||
|
||||
> ✅ Note: Add your CLI or unit tests to the `test/` directory.
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
||||
## 🧹 Code Guidelines
|
||||
|
||||
- Use `go fmt ./...` to format your code.
|
||||
- Use descriptive commit messages:
|
||||
- `feat`: New feature
|
||||
- `fix`: Bug fix
|
||||
- `docs`: Documentation only
|
||||
- `test`: Adding or updating tests
|
||||
- `refactor`: Code cleanup
|
||||
- `chore`: Maintenance tasks
|
||||
|
||||
## 📬 Communication
|
||||
|
||||
- **Slack:** Join us in [#harbor-cli](https://cloud-native.slack.com/messages/harbor-cli/)
|
||||
- **Issues:** Use [GitHub Issues](https://github.com/goharbor/harbor-cli/issues) for bugs, ideas, or questions.
|
||||
- **Mailing List:**
|
||||
- Users: [harbor-users@lists.cncf.io](https://lists.cncf.io/g/harbor-users)
|
||||
- Devs: [harbor-dev@lists.cncf.io](https://lists.cncf.io/g/harbor-dev)
|
||||
|
||||
## 📄 License
|
||||
|
||||
All contributions are under the [Apache 2.0 License](./LICENSE).
|
||||
|
||||
---
|
||||
|
||||
**Thank you for contributing to Harbor CLI! Your work helps improve the Harbor ecosystem for everyone. 🙌**
|
118
README.md
118
README.md
|
@ -1,37 +1,76 @@
|
|||

|
||||
|
||||
**Welcome to the Harbor CLI project! This powerful command-line tool facilitates seamless interaction with the Harbor container registry. It simplifies various tasks such as creating, updating, and managing projects, registries, and other resources in Harbor.**
|
||||

|
||||
|
||||
**Harbor CLI — a command-line interface for interacting with your Harbor container registry. A streamlined, user-friendly alternative to the WebUI, as your daily driver or for scripting and automation.**
|
||||
|
||||
[](https://artifacthub.io/packages/search?repo=harbor-cli)
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Fgoharbor%2Fharbor-cli?ref=badge_shield)
|
||||
[](https://codecov.io/gh/goharbor/harbor-cli)
|
||||
[](https://goreportcard.com/report/github.com/goharbor/harbor-cli)
|
||||
|
||||
# Project Scope 🧪
|
||||
# Scope 🧪
|
||||
|
||||
The Harbor CLI is designed to enhance your interaction with the Harbor container registry. Built on Golang, it offers a user-friendly interface to perform various tasks related to projects, registries, and more. Whether you're creating, updating, or managing resources, the Harbor CLI streamlines your workflow efficiently.
|
||||
1. CLI alternative to the WebUI
|
||||
2. Tool for scripting and automation of common repeatable Harbor tasks running on your machine or inside your pipeline
|
||||
|
||||
# Project Features 🤯
|
||||
# Features
|
||||
The project's first goal is to reach WebUI parity.
|
||||
|
||||
🔹 Get details about projects, registries, repositories and more <br>
|
||||
🔹 Create new projects, registries, and other resources <br>
|
||||
🔹 Delete projects, registries, and other resources <br>
|
||||
🔹 Run commands with various flags for enhanced functionality <br>
|
||||
🔹 More features coming soon... 🚧
|
||||
```shell
|
||||
✅ project Mange projects
|
||||
✅ repo Manage repositories
|
||||
✅ artifact Manage artifacts
|
||||
✅ label Manage labels
|
||||
✅ tag Manage tags
|
||||
✅ quota Manage quotas
|
||||
✅ webhook Manage webhook policies
|
||||
✅ robot Robot Account
|
||||
|
||||
✅ login Log in to Harbor registry
|
||||
✅ user Manage users
|
||||
|
||||
✅ registry Manage registries
|
||||
❌ replication Manage replication
|
||||
|
||||
✅ config Manage the config of the Harbor CLI
|
||||
✅ cve-allowlist Manage system CVE allowlist
|
||||
✅ health Get the health status of Harbor components
|
||||
✅ instance Manage preheat provider instances in Harbor
|
||||
✅ info Display detailed Harbor system, statistics, and CLI environment information
|
||||
|
||||
|
||||
✅ scanner scanner commands
|
||||
✅ schedule Schedule jobs in Harbor
|
||||
|
||||
✅ completion Generate the autocompletion script for the specified shell\
|
||||
✅ help Help about any command
|
||||
✅ version Version of Harbor CLI
|
||||
|
||||
```
|
||||
|
||||
# Installation
|
||||
|
||||
## Container
|
||||
|
||||
It is straightforward to use the Harbor CLI as a container. You can run the following command to use the Harbor CLI as a container:
|
||||
Running Harbor CLI as a container is simple. Use the following command to get started:
|
||||
|
||||
```shell
|
||||
docker run -ti --rm -v $HOME/.harbor/config.yaml:/root/.harbor/config.yaml registry.goharbor.io/harbor-cli/harbor-cli --help
|
||||
|
||||
docker run -ti --rm -v $HOME/.config/harbor-cli/config.yaml:/root/.config/harbor-cli/config.yaml \
|
||||
-e HARBOR_ENCRYPTION_KEY=$(echo "ThisIsAVeryLongPassword" | base64) \
|
||||
registry.goharbor.io/harbor-cli/harbor-cli \
|
||||
--help
|
||||
```
|
||||
Use the `HARBOR_ENCRYPTION_KEY` container environment variable as a base64-encoded 32-byte key for AES-256 encryption. This securely stores your harbor login password.
|
||||
|
||||
If you intend
|
||||
to run the CLI as a container,it is advised
|
||||
to set the following environment variables and to create an alias
|
||||
and append the alias to your .zshrc or .bashrc file
|
||||
|
||||
# Add the following command to create an alias and append the alias to your .zshrc or .bashrc file
|
||||
```shell
|
||||
echo "alias harbor='docker run -ti --rm -v \$HOME/.harbor/config.yaml:/root/.harbor/config.yaml registry.goharbor.io/harbor-cli/harbor-cli'" >> ~/.zshrc
|
||||
echo "export HARBOR_CLI_CONFIG=\$HOME/.config/harbor-cli/config.yaml" >> ~/.zshrc
|
||||
echo "export HARBOR_ENCRYPTION_KEY=\$(cat <path_to_32bit_private_key_file> | base64)" >> ~/.zshrc
|
||||
echo "alias harbor='docker run -ti --rm -v \$HARBOR_CLI_CONFIG:/root/.config/harbor-cli/config.yaml -e HARBOR_ENCRYPTION_KEY=\$HARBOR_ENCRYPTION_KEY registry.goharbor.io/harbor-cli/harbor-cli'" >> ~/.zshrc
|
||||
source ~/.zshrc # or restart your terminal
|
||||
```
|
||||
|
||||
|
@ -40,14 +79,14 @@ source ~/.zshrc # or restart your terminal
|
|||
|
||||
Harbor CLI will soon be published on Homebrew.
|
||||
Meantime, we recommend using Harbor in the Container
|
||||
or download the binary from the [releases page](https://github.com/goharbor/harbor-cli/releases)
|
||||
or downloading the binary from the [releases page](https://github.com/goharbor/harbor-cli/releases)
|
||||
|
||||
|
||||
|
||||
## Add the Harbor CLI to your Container Image
|
||||
|
||||
Using Curl or Wget isn't recommended
|
||||
for adding the Harbor CLI to your container.
|
||||
Using Curl or Wget isn't needed if you want to
|
||||
add the Harbor CLI to your container.
|
||||
Instead, we recommend copying the Harbor CLI from our official image
|
||||
by using the following Dockerfile:
|
||||
|
||||
|
@ -78,16 +117,23 @@ harbor help
|
|||
|
||||
|
||||
Available Commands:
|
||||
artifact Manage artifacts
|
||||
completion Generate the autocompletion script for the specified shell
|
||||
health Get the health status of Harbor components
|
||||
help Help about any command
|
||||
login Log in to Harbor registry
|
||||
project Manage projects and assign resources to them
|
||||
registry Manage registries
|
||||
repo Manage repositories
|
||||
user Manage users
|
||||
version Version of Harbor CLI
|
||||
artifact Manage artifacts
|
||||
completion Generate the autocompletion script for the specified shell
|
||||
config Manage the config of the Harbor CLI
|
||||
cve-allowlist Manage system CVE allowlist
|
||||
health Get the health status of Harbor components
|
||||
help Help about any command
|
||||
info Show the current credential information
|
||||
instance Manage preheat provider instances in Harbor
|
||||
label Manage labels in Harbor
|
||||
login Log in to Harbor registry
|
||||
project Manage projects and assign resources to them
|
||||
registry Manage registries
|
||||
repo Manage repositories
|
||||
schedule Schedule jobs in Harbor
|
||||
tag Manage tags in Harbor registry
|
||||
user Manage users
|
||||
version Version of Harbor CLI
|
||||
|
||||
Flags:
|
||||
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
|
||||
|
@ -103,15 +149,19 @@ Use "harbor [command] --help" for more information about a command.
|
|||
#### Config Management
|
||||
|
||||
##### Hierarchy
|
||||
Use the `--config` flag to specify a custom configuration file path (highest priority).
|
||||
```bash
|
||||
Use the `--config` flag to specify a custom configuration file path (the highest priority).
|
||||
|
||||
```bash
|
||||
harbor --config /path/to/custom/config.yaml artifact list
|
||||
```
|
||||
|
||||
If `--config` is not provided, Harbor CLI checks the `HARBOR_CLI_CONFIG` environment variable for the config file path.
|
||||
|
||||
```bash
|
||||
export HARBOR_CLI_CONFIG=/path/to/custom/config.yaml
|
||||
harbor artifact list
|
||||
```
|
||||
|
||||
If neither is set, it defaults to `$XDG_CONFIG_HOME/harbor-cli/config.yaml` or `$HOME/.config/harbor-cli/config.yaml` if `XDG_CONFIG_HOME` is unset.
|
||||
```bash
|
||||
harbor artifact list
|
||||
|
@ -178,9 +228,10 @@ Windows | ✅
|
|||
|
||||
# Build From Source
|
||||
|
||||
Make sure you have latest [Dagger](https://docs.dagger.io/) installed in your system.
|
||||
Make sure you have the latest [Dagger](https://docs.dagger.io/) installed in your system.
|
||||
|
||||
#### Using Dagger
|
||||
|
||||
```bash
|
||||
git clone https://github.com/goharbor/harbor-cli.git && cd harbor-cli
|
||||
dagger call build-dev --platform darwin/arm64 export --path=./harbor-cli
|
||||
|
@ -196,8 +247,8 @@ go build -o harbor-cli cmd/harbor/main.go
|
|||
|
||||
# Version Compatibility With Harbor
|
||||
|
||||
At the moment, the Harbor CLI is developed and tested with Harbor 2.11.
|
||||
The CLI should work with versions prior to 2.11,
|
||||
At the moment, the Harbor CLI is developed and tested with Harbor 2.13.
|
||||
The CLI should work with versions prior to 2.13,
|
||||
but not all functionalities may be available or work as expected.
|
||||
|
||||
Harbor <2.0.0 is not supported.
|
||||
|
@ -226,3 +277,4 @@ This project is maintained by the Harbor community. We thank all our contributor
|
|||
|
||||
For any questions or issues, please open an issue on our [GitHub Issues](https://github.com/goharbor/harbor-cli/issues) page.<br>
|
||||
Give a ⭐ if this project helped you, Thank YOU!
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
package artifact
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
|
@ -26,22 +28,27 @@ func DeleteArtifactCommand() *cobra.Command {
|
|||
Use: "delete",
|
||||
Short: "delete an artifact",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
var projectName, repoName, reference string
|
||||
if len(args) > 0 {
|
||||
projectName, repoName, reference := utils.ParseProjectRepoReference(args[0])
|
||||
err = api.DeleteArtifact(projectName, repoName, reference)
|
||||
projectName, repoName, reference, err = utils.ParseProjectRepoReference(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse project/repo/reference: %v", err)
|
||||
}
|
||||
} else {
|
||||
projectName := prompt.GetProjectNameFromUser()
|
||||
repoName := prompt.GetRepoNameFromUser(projectName)
|
||||
reference := prompt.GetReferenceFromUser(repoName, projectName)
|
||||
err = api.DeleteArtifact(projectName, repoName, reference)
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
repoName = prompt.GetRepoNameFromUser(projectName)
|
||||
reference = prompt.GetReferenceFromUser(repoName, projectName)
|
||||
}
|
||||
|
||||
err = api.DeleteArtifact(projectName, repoName, reference)
|
||||
if err != nil {
|
||||
log.Errorf("failed to delete an artifact: %v", err)
|
||||
return fmt.Errorf("failed to delete an artifact: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -30,8 +30,18 @@ func ListArtifactCommand() *cobra.Command {
|
|||
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "list artifacts within a repository",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Short: "List container artifacts (images, charts, etc.) in a Harbor repository with metadata",
|
||||
Long: `List all artifacts (e.g., container images, charts) within a given Harbor repository.
|
||||
Supports optional project/repository input in the form <project>/<repository>.
|
||||
Displays key artifact metadata including tags, digest, type, size, vulnerability count, and push time.
|
||||
|
||||
Examples:
|
||||
harbor-cli artifact list # Interactive prompt for project and repository
|
||||
harbor-cli artifact list library/nginx # Directly list artifacts in the nginx repo under 'library' project
|
||||
|
||||
Supports pagination, search queries, and sorting using flags.`,
|
||||
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if opts.PageSize > 100 {
|
||||
return fmt.Errorf("page size should be less than or equal to 100")
|
||||
|
@ -41,9 +51,15 @@ func ListArtifactCommand() *cobra.Command {
|
|||
var projectName, repoName string
|
||||
|
||||
if len(args) > 0 {
|
||||
projectName, repoName = utils.ParseProjectRepo(args[0])
|
||||
projectName, repoName, err = utils.ParseProjectRepo(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse project/repo: %v", err)
|
||||
}
|
||||
} else {
|
||||
projectName = prompt.GetProjectNameFromUser()
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
repoName = prompt.GetRepoNameFromUser(projectName)
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
package artifact
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
|
@ -44,21 +46,28 @@ func StartScanArtifactCommand() *cobra.Command {
|
|||
Short: "Start a scan of an artifact",
|
||||
Long: `Start a scan of an artifact in Harbor Repository`,
|
||||
Example: `harbor artifact scan start <project>/<repository>/<reference>`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
var projectName, repoName, reference string
|
||||
|
||||
if len(args) > 0 {
|
||||
projectName, repoName, reference := utils.ParseProjectRepoReference(args[0])
|
||||
err = api.StartScanArtifact(projectName, repoName, reference)
|
||||
projectName, repoName, reference, err = utils.ParseProjectRepoReference(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse project/repo/reference: %v", err)
|
||||
}
|
||||
} else {
|
||||
projectName := prompt.GetProjectNameFromUser()
|
||||
repoName := prompt.GetRepoNameFromUser(projectName)
|
||||
reference := prompt.GetReferenceFromUser(repoName, projectName)
|
||||
err = api.StartScanArtifact(projectName, repoName, reference)
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
repoName = prompt.GetRepoNameFromUser(projectName)
|
||||
reference = prompt.GetReferenceFromUser(repoName, projectName)
|
||||
}
|
||||
err = api.StartScanArtifact(projectName, repoName, reference)
|
||||
if err != nil {
|
||||
log.Errorf("failed to start scan of artifact: %v", err)
|
||||
return fmt.Errorf("failed to start scan of artifact: %v", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
|
@ -72,16 +81,24 @@ func StopScanArtifactCommand() *cobra.Command {
|
|||
Example: `harbor artifact scan stop <project>/<repository>/<reference>`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
var projectName, repoName, reference string
|
||||
|
||||
if len(args) > 0 {
|
||||
projectName, repoName, reference := utils.ParseProjectRepoReference(args[0])
|
||||
err = api.StopScanArtifact(projectName, repoName, reference)
|
||||
projectName, repoName, reference, err = utils.ParseProjectRepoReference(args[0])
|
||||
if err != nil {
|
||||
log.Errorf("failed to parse project/repo/reference: %v", err)
|
||||
}
|
||||
} else {
|
||||
projectName := prompt.GetProjectNameFromUser()
|
||||
repoName := prompt.GetRepoNameFromUser(projectName)
|
||||
reference := prompt.GetReferenceFromUser(repoName, projectName)
|
||||
err = api.StopScanArtifact(projectName, repoName, reference)
|
||||
var projectName string
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
repoName = prompt.GetRepoNameFromUser(projectName)
|
||||
reference = prompt.GetReferenceFromUser(repoName, projectName)
|
||||
}
|
||||
|
||||
err = api.StopScanArtifact(projectName, repoName, reference)
|
||||
if err != nil {
|
||||
log.Errorf("failed to stop scan of artifact: %v", err)
|
||||
}
|
||||
|
|
|
@ -48,19 +48,24 @@ func CreateTagsCmd() *cobra.Command {
|
|||
Example: `harbor artifact tags create <project>/<repository>/<reference> <tag>`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
|
||||
var projectName, repoName, reference string
|
||||
var tagName string
|
||||
if len(args) > 0 {
|
||||
projectName, repoName, reference := utils.ParseProjectRepoReference(args[0])
|
||||
tag := args[1]
|
||||
err = api.CreateTag(projectName, repoName, reference, tag)
|
||||
projectName, repoName, reference, err = utils.ParseProjectRepoReference(args[0])
|
||||
if err != nil {
|
||||
log.Errorf("failed to parse project/repo/reference: %v", err)
|
||||
}
|
||||
tagName = args[1]
|
||||
} else {
|
||||
var tagName string
|
||||
projectName := prompt.GetProjectNameFromUser()
|
||||
repoName := prompt.GetRepoNameFromUser(projectName)
|
||||
reference := prompt.GetReferenceFromUser(repoName, projectName)
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
repoName = prompt.GetRepoNameFromUser(projectName)
|
||||
reference = prompt.GetReferenceFromUser(repoName, projectName)
|
||||
create.CreateTagView(&tagName)
|
||||
err = api.CreateTag(projectName, repoName, reference, tagName)
|
||||
}
|
||||
err = api.CreateTag(projectName, repoName, reference, tagName)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create tag: %v", err)
|
||||
}
|
||||
|
@ -81,10 +86,19 @@ func ListTagsCmd() *cobra.Command {
|
|||
var projectName, repoName, reference string
|
||||
|
||||
if len(args) > 0 {
|
||||
projectName, repoName, reference = utils.ParseProjectRepoReference(args[0])
|
||||
projectName, repoName, reference, err = utils.ParseProjectRepoReference(args[0])
|
||||
if err != nil {
|
||||
log.Errorf("failed to parse project/repo/reference: %v", err)
|
||||
}
|
||||
} else {
|
||||
projectName = prompt.GetProjectNameFromUser()
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
repoName = prompt.GetRepoNameFromUser(projectName)
|
||||
if repoName == "" {
|
||||
return
|
||||
}
|
||||
reference = prompt.GetReferenceFromUser(repoName, projectName)
|
||||
}
|
||||
|
||||
|
@ -118,18 +132,24 @@ func DeleteTagsCmd() *cobra.Command {
|
|||
Example: `harbor artifact tags delete <project>/<repository>/<reference> <tag>`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
|
||||
var projectName, repoName, reference string
|
||||
var tagName string
|
||||
if len(args) > 0 {
|
||||
projectName, repoName, reference := utils.ParseProjectRepoReference(args[0])
|
||||
tag := args[1]
|
||||
err = api.DeleteTag(projectName, repoName, reference, tag)
|
||||
projectName, repoName, reference, err = utils.ParseProjectRepoReference(args[0])
|
||||
if err != nil {
|
||||
log.Errorf("failed to parse project/repo/reference: %v", err)
|
||||
}
|
||||
tagName = args[1]
|
||||
} else {
|
||||
projectName := prompt.GetProjectNameFromUser()
|
||||
repoName := prompt.GetRepoNameFromUser(projectName)
|
||||
reference := prompt.GetReferenceFromUser(repoName, projectName)
|
||||
tag := prompt.GetTagFromUser(repoName, projectName, reference)
|
||||
err = api.DeleteTag(projectName, repoName, reference, tag)
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
repoName = prompt.GetRepoNameFromUser(projectName)
|
||||
reference = prompt.GetReferenceFromUser(repoName, projectName)
|
||||
tagName = prompt.GetTagFromUser(repoName, projectName, reference)
|
||||
}
|
||||
err = api.DeleteTag(projectName, repoName, reference, tagName)
|
||||
if err != nil {
|
||||
log.Errorf("failed to delete tag: %v", err)
|
||||
}
|
||||
|
|
|
@ -29,21 +29,36 @@ func ViewArtifactCommmand() *cobra.Command {
|
|||
Use: "view",
|
||||
Short: "Get information of an artifact",
|
||||
Long: `Get information of an artifact`,
|
||||
Example: `harbor artifact view <project>/<repository>/<reference>`,
|
||||
Example: `harbor artifact view <project>/<repository>:<tag> OR harbor artifact view <project>/<repository>@<digest>`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
var projectName, repoName, reference string
|
||||
var artifact *artifact.GetArtifactOK
|
||||
|
||||
if len(args) > 0 {
|
||||
projectName, repoName, reference = utils.ParseProjectRepoReference(args[0])
|
||||
projectName, repoName, reference, err = utils.ParseProjectRepoReference(args[0])
|
||||
if err != nil {
|
||||
log.Errorf("failed to parse project/repo/reference: %v", err)
|
||||
}
|
||||
} else {
|
||||
projectName = prompt.GetProjectNameFromUser()
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
|
||||
return
|
||||
}
|
||||
repoName = prompt.GetRepoNameFromUser(projectName)
|
||||
reference = prompt.GetReferenceFromUser(repoName, projectName)
|
||||
}
|
||||
|
||||
artifact, err = api.ViewArtifact(projectName, repoName, reference)
|
||||
if reference == "" {
|
||||
if len(args) > 0 {
|
||||
log.Errorf("Invalid artifact reference format: %s", args[0])
|
||||
} else {
|
||||
log.Error("Invalid artifact reference format: no arguments provided")
|
||||
}
|
||||
}
|
||||
|
||||
artifact, err = api.ViewArtifact(projectName, repoName, reference, false)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("failed to get info of an artifact: %v", err)
|
||||
|
|
|
@ -15,17 +15,26 @@ package root
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/artifact"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/config"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/context"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/cve"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/instance"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/labels"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/project"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/quota"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/registry"
|
||||
repositry "github.com/goharbor/harbor-cli/cmd/harbor/root/repository"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/replication"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/repository"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/robot"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/scan_all"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/scanner"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/schedule"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/tag"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/user"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/webhook"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
@ -58,17 +67,16 @@ harbor help
|
|||
utils.InitConfig(cfgFile, userSpecifiedConfig)
|
||||
|
||||
// Conditionally set the timestamp format only in verbose mode
|
||||
formatter := &logrus.TextFormatter{}
|
||||
|
||||
if verbose {
|
||||
logrus.SetFormatter(&logrus.TextFormatter{
|
||||
FullTimestamp: true,
|
||||
TimestampFormat: time.RFC3339,
|
||||
})
|
||||
formatter.FullTimestamp = true
|
||||
formatter.TimestampFormat = time.RFC3339
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
} else {
|
||||
// No timestamp format for non-verbose
|
||||
logrus.SetFormatter(&logrus.TextFormatter{
|
||||
DisableTimestamp: true,
|
||||
})
|
||||
logrus.SetOutput(io.Discard)
|
||||
}
|
||||
logrus.SetFormatter(formatter)
|
||||
|
||||
return nil
|
||||
},
|
||||
|
@ -88,21 +96,102 @@ harbor help
|
|||
fmt.Println(err.Error())
|
||||
}
|
||||
|
||||
root.AddCommand(
|
||||
versionCommand(),
|
||||
LoginCommand(),
|
||||
config.Config(),
|
||||
project.Project(),
|
||||
registry.Registry(),
|
||||
repositry.Repository(),
|
||||
user.User(),
|
||||
artifact.Artifact(),
|
||||
tag.TagCommand(),
|
||||
HealthCommand(),
|
||||
schedule.Schedule(),
|
||||
labels.Labels(),
|
||||
InfoCommand(),
|
||||
)
|
||||
root.AddGroup(&cobra.Group{ID: "core", Title: "Core:"})
|
||||
root.AddGroup(&cobra.Group{ID: "access", Title: "Access:"})
|
||||
root.AddGroup(&cobra.Group{ID: "system", Title: "System:"})
|
||||
root.AddGroup(&cobra.Group{ID: "utils", Title: "Utility:"})
|
||||
|
||||
// Core
|
||||
cmd := InfoCommand()
|
||||
cmd.GroupID = "core"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = project.Project()
|
||||
cmd.GroupID = "core"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = repository.Repository()
|
||||
cmd.GroupID = "core"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = artifact.Artifact()
|
||||
cmd.GroupID = "core"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = tag.TagCommand()
|
||||
cmd.GroupID = "core"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = labels.Labels()
|
||||
cmd.GroupID = "core"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = quota.Quota()
|
||||
cmd.GroupID = "core"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = cve.CVEAllowlist()
|
||||
cmd.GroupID = "core"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = webhook.Webhook()
|
||||
cmd.GroupID = "core"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = robot.Robot()
|
||||
cmd.GroupID = "core"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
// Access
|
||||
cmd = LoginCommand()
|
||||
cmd.GroupID = "access"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = user.User()
|
||||
cmd.GroupID = "access"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
// System
|
||||
cmd = context.Context()
|
||||
cmd.GroupID = "system"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = HealthCommand()
|
||||
cmd.GroupID = "system"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = instance.Instance()
|
||||
cmd.GroupID = "system"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = registry.Registry()
|
||||
cmd.GroupID = "system"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = replication.Replication()
|
||||
cmd.GroupID = "system"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = scanner.Scanner()
|
||||
cmd.GroupID = "system"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = scan_all.ScanAll()
|
||||
cmd.GroupID = "system"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = schedule.Schedule()
|
||||
cmd.GroupID = "system"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
// Utils
|
||||
cmd = versionCommand()
|
||||
cmd.GroupID = "utils"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = Logs()
|
||||
cmd.GroupID = "utils"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
return root
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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.
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1,60 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package cve
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/cveallowlist/update"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func AddCveAllowlistCommand() *cobra.Command {
|
||||
var opts update.UpdateView
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "add",
|
||||
Short: "Add cve allowlist",
|
||||
Long: "Create allowlist of CVEs to ignore during vulnerability scanning",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
updateView := &update.UpdateView{
|
||||
CveId: opts.CveId,
|
||||
IsExpire: opts.IsExpire,
|
||||
ExpireDate: opts.ExpireDate,
|
||||
}
|
||||
|
||||
err = updatecveView(updateView)
|
||||
if err != nil {
|
||||
log.Errorf("failed to add cveallowlist: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.IsExpire, "isexpire", "i", false, "Indicates whether the CVE entries should have an expiration date. Set to true to specify an expiration date")
|
||||
flags.StringVarP(&opts.CveId, "cveid", "n", "", "Comma-separated list of CVE IDs to be added to the allowlist")
|
||||
flags.StringVarP(&opts.ExpireDate, "expiredate", "d", "", "Specifies the expiration date for the CVE entries in the format 'YYYY-MM-DD'")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func updatecveView(updateView *update.UpdateView) error {
|
||||
if updateView == nil {
|
||||
updateView = &update.UpdateView{}
|
||||
}
|
||||
|
||||
update.UpdateCveView(updateView)
|
||||
return api.UpdateSystemCve(*updateView)
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package cve
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func CVEAllowlist() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "cve-allowlist",
|
||||
Short: "Manage system CVE allowlist",
|
||||
Long: `Managing CVE lists that are intentionally excluded from vulnerability scanning`,
|
||||
Example: `harbor cve-allowlist list`,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
ListCveCommand(),
|
||||
AddCveAllowlistCommand(),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package cve
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/cveallowlist/list"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func ListCveCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List system level allowlist of cve",
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cve, err := api.ListSystemCve()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get system cve list: %v", err)
|
||||
}
|
||||
FormatFlag := viper.GetString("output-format")
|
||||
if FormatFlag != "" {
|
||||
err = utils.PrintFormat(cve, FormatFlag)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to print cve list: %v", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
list.ListSystemCve(cve.Payload)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -15,8 +15,10 @@ package root
|
|||
|
||||
import (
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/health"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func HealthCommand() *cobra.Command {
|
||||
|
@ -32,7 +34,15 @@ func HealthCommand() *cobra.Command {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
health.PrintHealthStatus(status)
|
||||
FormatFlag := viper.GetString("output-format")
|
||||
if FormatFlag != "" {
|
||||
err = utils.PrintFormat(status, FormatFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
health.PrintHealthStatus(status)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Example: ` # Get the health status of Harbor components`,
|
||||
|
|
|
@ -16,88 +16,73 @@ package root
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/go-client/pkg/sdk/v2.0/client/systeminfo"
|
||||
"github.com/goharbor/go-client/pkg/sdk/v2.0/client/user"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/internal/version"
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/info/list"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Lists the info of the Harbor system
|
||||
func InfoCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "info",
|
||||
Short: "Show the current credential information",
|
||||
Short: "Display detailed Harbor system, statistics, and CLI environment information",
|
||||
Long: `The 'info' command retrieves and displays general information about the Harbor instance,
|
||||
including system metadata, storage statistics, and CLI environment details such as user identity,
|
||||
registry address, and CLI version.
|
||||
|
||||
The output can be formatted as table (default), JSON, or YAML using the '--output-format' flag.`,
|
||||
Example: ` harbor info
|
||||
harbor info --output-format json
|
||||
harbor info -o yaml`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
currentCredential := viper.GetString("current-credential-name")
|
||||
if currentCredential == "" {
|
||||
return fmt.Errorf("no active credentials found")
|
||||
var cliinfo *api.CLIInfo
|
||||
var err error
|
||||
generalInfo, err := api.GetSystemInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var registryAddress string
|
||||
creds := viper.Get("credentials").([]interface{})
|
||||
for _, cred := range creds {
|
||||
c := cred.(map[string]interface{})
|
||||
if c["name"] == currentCredential {
|
||||
registryAddress = c["serveraddress"].(string)
|
||||
break
|
||||
stats, err := api.GetStats()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sysVolume, err := api.GetSystemVolumes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cliVersion := version.Version
|
||||
OSinfo := version.System
|
||||
|
||||
cliinfo, err = api.GetCLIInfo()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to get CLI info: %w", err)
|
||||
}
|
||||
systemInfo := list.CreateSystemInfo(
|
||||
generalInfo.Payload,
|
||||
stats.Payload,
|
||||
sysVolume.Payload,
|
||||
cliinfo,
|
||||
cliVersion,
|
||||
OSinfo,
|
||||
)
|
||||
|
||||
FormatFlag := viper.GetString("output-format")
|
||||
if FormatFlag != "" {
|
||||
err = utils.PrintFormat(systemInfo, FormatFlag)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
} else {
|
||||
list.ListInfo(&systemInfo)
|
||||
}
|
||||
|
||||
if registryAddress == "" {
|
||||
return fmt.Errorf("registry address not found for current credential: %s", currentCredential)
|
||||
}
|
||||
|
||||
ctx, client, err := utils.ContextWithClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create Harbor client: %v", err)
|
||||
}
|
||||
|
||||
userInfo, err := client.User.GetCurrentUserInfo(ctx, &user.GetCurrentUserInfoParams{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current user info: %v", err)
|
||||
}
|
||||
|
||||
isSysAdmin := userInfo.Payload.SysadminFlag
|
||||
|
||||
sysInfo, err := client.Systeminfo.GetSystemInfo(ctx, &systeminfo.GetSystemInfoParams{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get system info: %v", err)
|
||||
}
|
||||
harborVersion := sysInfo.Payload.HarborVersion
|
||||
|
||||
fmt.Println("\nHarbor CLI Info:")
|
||||
fmt.Println("==================")
|
||||
fmt.Printf("Logged in as: %s\n", userInfo.Payload.Username)
|
||||
fmt.Printf("Registry: %s\n", registryAddress)
|
||||
fmt.Printf("Harbor Version: %s\n", *harborVersion)
|
||||
fmt.Printf("Connected as Admin: %s\n", roleString(isSysAdmin))
|
||||
|
||||
// Previously logged-in registries
|
||||
fmt.Println("\nPreviously Logged in to the following registries:")
|
||||
previousRegistriesMap := make(map[string]struct{})
|
||||
for _, cred := range creds {
|
||||
c := cred.(map[string]interface{})
|
||||
if registry, ok := c["serveraddress"].(string); ok {
|
||||
previousRegistriesMap[registry] = struct{}{}
|
||||
}
|
||||
}
|
||||
for registry := range previousRegistriesMap {
|
||||
fmt.Printf("- %s\n", registry)
|
||||
}
|
||||
|
||||
fmt.Printf("\nCLI Version: %s\n", version.Version)
|
||||
fmt.Printf("OS: %s\n", version.System)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func roleString(isSysAdmin bool) string {
|
||||
if isSysAdmin {
|
||||
return "Yes"
|
||||
}
|
||||
return "No"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package instance
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
func Instance() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "instance",
|
||||
Short: "Manage preheat provider instances in Harbor",
|
||||
Long: `Manage preheat provider instances used by Harbor for pre-distributing container images.
|
||||
These instances represent external services such as Dragonfly or Kraken that help preheat images across nodes.`,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
CreateInstanceCommand(),
|
||||
DeleteInstanceCommand(),
|
||||
ListInstanceCommand(),
|
||||
)
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package instance
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/instance/create"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func CreateInstanceCommand() *cobra.Command {
|
||||
var opts create.CreateView
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create a new preheat provider instance in Harbor",
|
||||
Long: `Create a new preheat provider instance within Harbor for distributing container images.
|
||||
The instance can be an external service such as Dragonfly, Kraken, or any custom provider.
|
||||
You will need to provide the instance's name, vendor, endpoint, and optionally other details such as authentication and security options.`,
|
||||
Example: ` harbor-cli instance create --name my-instance --provider Dragonfly --url http://dragonfly.local --description "My preheat provider instance" --enable=true`,
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
createView := &create.CreateView{
|
||||
Name: opts.Name,
|
||||
Vendor: opts.Vendor,
|
||||
Description: opts.Description,
|
||||
Endpoint: opts.Endpoint,
|
||||
Insecure: opts.Insecure,
|
||||
Enabled: opts.Enabled,
|
||||
AuthMode: opts.AuthMode,
|
||||
AuthInfo: opts.AuthInfo,
|
||||
}
|
||||
|
||||
if opts.Name != "" && opts.Vendor != "" && opts.Endpoint != "" {
|
||||
err = api.CreateInstance(opts)
|
||||
} else {
|
||||
err = createInstanceView(createView)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("failed to create instance: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opts.Name, "name", "n", "", "Name of the instance")
|
||||
flags.StringVarP(&opts.Vendor, "provider", "p", "", "Provider for the instance")
|
||||
flags.StringVarP(&opts.Endpoint, "url", "u", "", "URL for the instance")
|
||||
flags.StringVarP(&opts.Description, "description", "", "", "Description of the instance")
|
||||
flags.BoolVarP(&opts.Insecure, "insecure", "i", true, "Whether or not the certificate will be verified when Harbor tries to access the server")
|
||||
flags.BoolVarP(&opts.Enabled, "enable", "", true, "Whether it is enabled or not")
|
||||
flags.StringVarP(&opts.AuthMode, "authmode", "a", "NONE", "Choosing different types of authentication method")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func createInstanceView(createView *create.CreateView) error {
|
||||
if createView == nil {
|
||||
createView = &create.CreateView{}
|
||||
}
|
||||
|
||||
create.CreateInstanceView(createView)
|
||||
return api.CreateInstance(*createView)
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package instance
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func DeleteInstanceCommand() *cobra.Command {
|
||||
var instanceID int64
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete",
|
||||
Short: "Delete a preheat provider instance by its name or ID",
|
||||
Long: `Delete a preheat provider instance from Harbor. You can specify the instance name or ID directly as an argument.
|
||||
If no argument is provided, you will be prompted to select an instance from a list of available instances.`,
|
||||
Example: ` harbor-cli instance delete my-instance
|
||||
harbor-cli instance delete 12345`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
var instanceName string
|
||||
|
||||
if instanceID != -1 {
|
||||
instanceName, err = api.GetInstanceNameByID(instanceID)
|
||||
if err != nil {
|
||||
log.Errorf("%v", err)
|
||||
return
|
||||
}
|
||||
} else if len(args) > 0 {
|
||||
instanceName = args[0]
|
||||
} else {
|
||||
instanceName = prompt.GetInstanceFromUser()
|
||||
}
|
||||
err = api.DeleteInstance(instanceName)
|
||||
if err != nil {
|
||||
log.Errorf("failed to delete instance: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
cmd.Flags().Int64VarP(&instanceID, "id", "i", -1, "ID of the instance to delete")
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package instance
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/instance/list"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func ListInstanceCommand() *cobra.Command {
|
||||
var opts api.ListFlags
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all preheat provider instances in Harbor",
|
||||
Long: `List all preheat provider instances registered in Harbor. You can paginate the results,
|
||||
filter them using a query string, and sort them in ascending or descending order.
|
||||
This command provides an easy way to view all instances along with their details.`,
|
||||
Example: ` harbor-cli instance list --page 1 --page-size 10
|
||||
harbor-cli instance list --query "name=my-instance" --sort "asc"`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
instance, err := api.ListInstance(opts)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get instance list: %v", err)
|
||||
}
|
||||
FormatFlag := viper.GetString("output-format")
|
||||
if FormatFlag != "" {
|
||||
err = utils.PrintFormat(instance, FormatFlag)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to print config: %v", err)
|
||||
}
|
||||
} else {
|
||||
list.ListInstance(instance.Payload)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.Int64VarP(&opts.Page, "page", "", 1, "Page number")
|
||||
flags.Int64VarP(&opts.PageSize, "page-size", "", 10, "Size of per page")
|
||||
flags.StringVarP(&opts.Q, "query", "q", "", "Query string to query resources")
|
||||
flags.StringVarP(&opts.Sort, "sort", "", "", "Sort the resource list in ascending or descending order")
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"))
|
||||
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -15,18 +15,18 @@ package config
|
|||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
func Config() *cobra.Command {
|
||||
var isID bool
|
||||
|
||||
func ProjectConfigCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Manage the config of the Harbor Cli",
|
||||
Long: `Manage repositories in Harbor config`,
|
||||
Short: "Manage project configuration",
|
||||
}
|
||||
cmd.AddCommand(
|
||||
ListConfigCommand(),
|
||||
GetConfigItemCommand(),
|
||||
UpdateConfigItemCommand(),
|
||||
DeleteConfigItemCommand(),
|
||||
UpdateProjectConfigCmd(),
|
||||
ListProjectConfigCmd(),
|
||||
)
|
||||
cmd.PersistentFlags().BoolVarP(&isID, "id", "", false, "Use project ID instead of name")
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -14,7 +14,10 @@
|
|||
package project
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/project/create"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
@ -28,37 +31,51 @@ func CreateProjectCommand() *cobra.Command {
|
|||
Use: "create [project name]",
|
||||
Short: "create project",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
createView := &create.CreateView{
|
||||
ProjectName: opts.ProjectName,
|
||||
Public: opts.Public,
|
||||
RegistryID: opts.RegistryID,
|
||||
StorageLimit: opts.StorageLimit,
|
||||
ProxyCache: false,
|
||||
}
|
||||
var ProjectName string
|
||||
if len(args) > 0 {
|
||||
opts.ProjectName = args[0]
|
||||
}
|
||||
|
||||
if opts.ProxyCache && opts.RegistryID == "" {
|
||||
log.Errorf("Use the --registry-id flag with a registry ID")
|
||||
} else {
|
||||
err = api.CreateProject(opts)
|
||||
}
|
||||
if opts.ProxyCache && opts.RegistryID == "" {
|
||||
return fmt.Errorf("proxy cache selected but no registry ID provided. Use --registry-id")
|
||||
}
|
||||
|
||||
if !opts.ProxyCache && opts.RegistryID != "" {
|
||||
return fmt.Errorf("registry ID should only be provided when proxy-cache is enabled")
|
||||
}
|
||||
|
||||
if opts.ProjectName != "" && opts.StorageLimit != "" {
|
||||
log.Debug("Attempting to create project using flags...")
|
||||
err = api.CreateProject(opts)
|
||||
ProjectName = opts.ProjectName
|
||||
} else {
|
||||
log.Debug("Switching to interactive view...")
|
||||
createView := &create.CreateView{
|
||||
ProjectName: opts.ProjectName,
|
||||
Public: opts.Public,
|
||||
RegistryID: opts.RegistryID,
|
||||
StorageLimit: opts.StorageLimit,
|
||||
ProxyCache: opts.ProxyCache,
|
||||
}
|
||||
|
||||
err = createProjectView(createView)
|
||||
ProjectName = createView.ProjectName
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("failed to create project: %v", err)
|
||||
return fmt.Errorf("failed to create project: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
fmt.Printf("Project '%s' created successfully\n", ProjectName)
|
||||
return nil
|
||||
}}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.Public, "public", "", false, "Project is public or private")
|
||||
flags.StringVarP(&opts.RegistryID, "registry-id", "", "", "ID of referenced registry when creating the proxy cache project")
|
||||
flags.StringVarP(&opts.StorageLimit, "storage-limit", "", "-1", "Storage quota of the project")
|
||||
flags.StringVarP(&opts.StorageLimit, "storage-limit", "", "", "Storage quota of the project")
|
||||
flags.BoolVarP(&opts.ProxyCache, "proxy-cache", "", false, "Whether the project is a proxy cache project")
|
||||
|
||||
return cmd
|
||||
|
@ -74,7 +91,9 @@ func createProjectView(createView *create.CreateView) error {
|
|||
}
|
||||
}
|
||||
|
||||
create.CreateProjectView(createView)
|
||||
|
||||
err := create.CreateProjectView(createView)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return api.CreateProject(*createView)
|
||||
}
|
||||
|
|
|
@ -14,15 +14,18 @@
|
|||
package project
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// DeleteProjectCommand creates a new `harbor project delete` command
|
||||
// DeleteProjectCommand creates a new `harbor delete project` command
|
||||
func DeleteProjectCommand() *cobra.Command {
|
||||
var forceDelete bool
|
||||
var projectID string
|
||||
|
@ -30,59 +33,101 @@ func DeleteProjectCommand() *cobra.Command {
|
|||
cmd := &cobra.Command{
|
||||
Use: "delete",
|
||||
Short: "Delete project by name or ID",
|
||||
Example: "harbor project delete [projectname] or harbor project delete --project-id [projectid]",
|
||||
Long: "Delete project by name or ID. If no arguments are provided, it will prompt for the project name. Use --project-id to specify the project ID directly. The --force flag will delete all repositories and artifacts within the project.",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Example: "harbor project delete [projectname1] [projectname2] or harbor project delete --project-id [projectid]",
|
||||
Long: "Delete project by name or ID. Multiple projects can be deleted by providing their names as arguments. If no arguments are provided, it will prompt for the project name. Use --project-id to specify the project ID for single project directly. The --force flag will delete all repositories and artifacts within the project.",
|
||||
Args: cobra.MinimumNArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var wg sync.WaitGroup
|
||||
errChan := make(chan error, len(args))
|
||||
var mu sync.Mutex
|
||||
|
||||
successfulDeletes := []string{}
|
||||
failedDeletes := map[string]string{}
|
||||
|
||||
if projectID != "" {
|
||||
if len(args) > 0 {
|
||||
return fmt.Errorf("--project-id cannot be used with additional arguments")
|
||||
}
|
||||
if _, err := strconv.Atoi(projectID); err != nil {
|
||||
return fmt.Errorf("--project-id must be a numeric value")
|
||||
}
|
||||
}
|
||||
|
||||
if projectID != "" {
|
||||
log.Debugf("Deleting project with ID: %s", projectID)
|
||||
wg.Add(1)
|
||||
go func(id string) {
|
||||
defer wg.Done()
|
||||
if err := api.DeleteProject(id, forceDelete, true); err != nil {
|
||||
errChan <- err
|
||||
mu.Lock()
|
||||
failedDeletes[id] = utils.ParseHarborErrorMsg(err)
|
||||
mu.Unlock()
|
||||
} else {
|
||||
mu.Lock()
|
||||
successfulDeletes = append(successfulDeletes, id)
|
||||
mu.Unlock()
|
||||
}
|
||||
}(projectID)
|
||||
} else if len(args) > 0 {
|
||||
// Delete by project name from args
|
||||
log.Debugf("Deleting %d projects from args...", len(args))
|
||||
for _, projectName := range args {
|
||||
pn := projectName
|
||||
log.Debugf("Initiating delete for project: %s", pn)
|
||||
wg.Add(1)
|
||||
go func(name string) {
|
||||
go func(projectName string) {
|
||||
defer wg.Done()
|
||||
if err := api.DeleteProject(name, forceDelete, false); err != nil {
|
||||
errChan <- err
|
||||
log.Debugf("Deleting project '%s' with force=%v", projectName, forceDelete)
|
||||
if err := api.DeleteProject(projectName, forceDelete, false); err != nil {
|
||||
mu.Lock()
|
||||
failedDeletes[projectName] = utils.ParseHarborErrorMsg(err)
|
||||
mu.Unlock()
|
||||
} else {
|
||||
mu.Lock()
|
||||
successfulDeletes = append(successfulDeletes, projectName)
|
||||
mu.Unlock()
|
||||
}
|
||||
}(projectName)
|
||||
}(pn)
|
||||
}
|
||||
} else {
|
||||
projectName := prompt.GetProjectNameFromUser()
|
||||
// If no arguments provided, prompt user for project name
|
||||
log.Debug("No arguments provided. Prompting user for project name.")
|
||||
projectName, err := prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
log.Debugf("User input project: %s", projectName)
|
||||
log.Debugf("Deleting project '%s' with force=%v", projectName, forceDelete)
|
||||
if err := api.DeleteProject(projectName, forceDelete, false); err != nil {
|
||||
log.Errorf("failed to delete project: %v", err)
|
||||
return fmt.Errorf("failed to delete project: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
fmt.Printf("Project '%s' deleted successfully\n", projectName)
|
||||
return nil
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if len(successfulDeletes) > 0 {
|
||||
fmt.Println("Successfully deleted projects:")
|
||||
for _, name := range successfulDeletes {
|
||||
fmt.Printf(" - %s\n", name)
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(errChan)
|
||||
}()
|
||||
|
||||
var finalErr error
|
||||
for err := range errChan {
|
||||
if finalErr == nil {
|
||||
finalErr = err
|
||||
} else {
|
||||
log.Errorf("Error: %v", err)
|
||||
if len(failedDeletes) > 0 {
|
||||
fmt.Println("Failed to delete projects:")
|
||||
for name, reason := range failedDeletes {
|
||||
fmt.Printf(" - %s: %s\n", name, reason)
|
||||
}
|
||||
return fmt.Errorf("failed to delete %d project(s)", len(failedDeletes))
|
||||
}
|
||||
if finalErr != nil {
|
||||
log.Errorf("failed to delete some projects: %v", finalErr)
|
||||
}
|
||||
|
||||
log.Debug("All requested projects deleted successfully.")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVar(&forceDelete, "force", false, "Deletes all repositories and artifacts within the project")
|
||||
flags.BoolVar(&forceDelete, "force", false, "Forcefully delete all repositories, artifacts, and policies in the project. Use with extreme caution—this action is irreversible.")
|
||||
flags.StringVar(&projectID, "project-id", "", "Specify project ID instead of project name")
|
||||
|
||||
return cmd
|
||||
|
|
|
@ -37,6 +37,8 @@ func ListProjectCommand() *cobra.Command {
|
|||
Short: "List projects",
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
log.Debug("Starting project list command")
|
||||
|
||||
if opts.PageSize > 100 {
|
||||
return fmt.Errorf("page size should be less than or equal to 100")
|
||||
}
|
||||
|
@ -44,35 +46,43 @@ func ListProjectCommand() *cobra.Command {
|
|||
if private && public {
|
||||
return fmt.Errorf("Cannot specify both --private and --public flags")
|
||||
}
|
||||
|
||||
var listFunc func(...api.ListFlags) (project.ListProjectsOK, error)
|
||||
if private {
|
||||
log.Debug("Using private project list function")
|
||||
opts.Public = false
|
||||
listFunc = api.ListProject
|
||||
} else if public {
|
||||
log.Debug("Using public project list function")
|
||||
opts.Public = true
|
||||
listFunc = api.ListProject
|
||||
} else {
|
||||
log.Debug("Using list all projects function")
|
||||
listFunc = api.ListAllProjects
|
||||
}
|
||||
|
||||
log.Debug("Fetching projects...")
|
||||
allProjects, err = fetchProjects(listFunc, opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get projects list: %v", err)
|
||||
return fmt.Errorf("failed to get projects list: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
|
||||
log.WithField("count", len(allProjects)).Debug("Number of projects fetched")
|
||||
if len(allProjects) == 0 {
|
||||
log.Info("No projects found")
|
||||
return nil
|
||||
}
|
||||
formatFlag := viper.GetString("output-format")
|
||||
if formatFlag != "" {
|
||||
log.WithField("output_format", formatFlag).Debug("Output format selected")
|
||||
err = utils.PrintFormat(allProjects, formatFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Debug("Listing projects using default view")
|
||||
list.ListProjects(allProjects)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
@ -92,24 +102,37 @@ func ListProjectCommand() *cobra.Command {
|
|||
func fetchProjects(listFunc func(...api.ListFlags) (project.ListProjectsOK, error), opts api.ListFlags) ([]*models.Project, error) {
|
||||
var allProjects []*models.Project
|
||||
if opts.PageSize == 0 {
|
||||
log.Debug("Page size is 0, will fetch all pages")
|
||||
opts.PageSize = 100
|
||||
opts.Page = 1
|
||||
|
||||
for {
|
||||
log.WithFields(log.Fields{
|
||||
"page": opts.Page,
|
||||
"page_size": opts.PageSize,
|
||||
}).Debug("Fetching next page of projects")
|
||||
|
||||
projects, err := listFunc(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.WithField("fetched_count", len(projects.Payload)).Debug("Fetched projects from current page")
|
||||
allProjects = append(allProjects, projects.Payload...)
|
||||
|
||||
if len(projects.Payload) < int(opts.PageSize) {
|
||||
log.Debug("Last page reached, stopping pagination")
|
||||
break
|
||||
}
|
||||
|
||||
opts.Page++
|
||||
}
|
||||
} else {
|
||||
log.WithFields(log.Fields{
|
||||
"page": opts.Page,
|
||||
"page_size": opts.PageSize,
|
||||
}).Debug("Fetching projects with user-defined pagination")
|
||||
|
||||
projects, err := listFunc(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
package project
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
proj "github.com/goharbor/go-client/pkg/sdk/v2.0/client/project"
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
|
@ -32,30 +34,49 @@ func LogsProjectCommmand() *cobra.Command {
|
|||
Use: "logs",
|
||||
Short: "get project logs",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
log.Debug("Starting execution of 'logs' command")
|
||||
var err error
|
||||
var resp *proj.GetLogsOK
|
||||
var projectName string
|
||||
|
||||
if len(args) > 0 {
|
||||
resp, err = api.LogsProject(args[0])
|
||||
projectName = args[0]
|
||||
log.Debugf("Project name provided as argument: %s", projectName)
|
||||
} else {
|
||||
projectName := prompt.GetProjectNameFromUser()
|
||||
resp, err = api.LogsProject(projectName)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get project logs: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
FormatFlag := viper.GetString("output-format")
|
||||
if FormatFlag != "" {
|
||||
err = utils.PrintFormat(resp, FormatFlag)
|
||||
log.Debug("No project name argument provided, prompting user...")
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return fmt.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
log.Debugf("Project name received from prompt: %s", projectName)
|
||||
}
|
||||
|
||||
log.Debugf("Checking if project '%s' exists...", projectName)
|
||||
projectExists, err := api.CheckProject(projectName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find project: %v ", utils.ParseHarborErrorMsg(err))
|
||||
} else if !projectExists {
|
||||
return fmt.Errorf("project %s does not exist", projectName)
|
||||
}
|
||||
log.Debugf("Fetching logs for project: %s", projectName)
|
||||
resp, err = api.LogsProject(projectName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get project logs: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
|
||||
formatFlag := viper.GetString("output-format")
|
||||
if formatFlag != "" {
|
||||
log.WithField("output_format", formatFlag).Debug("Output format selected")
|
||||
err = utils.PrintFormat(resp, formatFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Debug("Listing project logs using default view")
|
||||
auditLog.LogsProject(resp.Payload)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -14,6 +14,8 @@
|
|||
package project
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/project/list"
|
||||
|
@ -27,21 +29,27 @@ func SearchProjectCommand() *cobra.Command {
|
|||
Use: "search",
|
||||
Short: "search project based on their names",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
log.Debug("Starting project search command")
|
||||
projects, err := api.SearchProject(args[0])
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get projects: %v", err)
|
||||
return fmt.Errorf("failed to get projects: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
log.Debugf("Found %d projects", len(projects.Payload.Project))
|
||||
if len(projects.Payload.Project) == 0 {
|
||||
return fmt.Errorf("No projects found with name similar to : %s", args[0])
|
||||
}
|
||||
|
||||
FormatFlag := viper.GetString("output-format")
|
||||
if FormatFlag != "" {
|
||||
err = utils.PrintFormat(projects, FormatFlag)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
list.SearchProjects(projects.Payload.Project)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
package project
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/go-client/pkg/sdk/v2.0/client/project"
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
|
@ -30,34 +32,45 @@ func ViewCommand() *cobra.Command {
|
|||
Use: "view [NAME|ID]",
|
||||
Short: "get project by name or id",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
var projectNameOrID string
|
||||
var projectName string
|
||||
var project *project.GetProjectOK
|
||||
|
||||
if len(args) > 0 {
|
||||
projectNameOrID = args[0]
|
||||
log.Debugf("Project name provided: %s", args[0])
|
||||
projectName = args[0]
|
||||
} else {
|
||||
projectNameOrID = prompt.GetProjectNameFromUser()
|
||||
isID = false
|
||||
log.Debug("No project name provided, prompting user")
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
}
|
||||
|
||||
project, err = api.GetProject(projectNameOrID, isID)
|
||||
log.Debugf("Checking existence of project: %s", projectName)
|
||||
projectExists, err := api.CheckProject(projectName)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get project: %v", err)
|
||||
return
|
||||
return fmt.Errorf("failed to find project: %v ", utils.ParseHarborErrorMsg(err))
|
||||
} else if !projectExists {
|
||||
return fmt.Errorf("project %s does not exist", projectName)
|
||||
}
|
||||
log.Debugf("Project %s exists", projectName)
|
||||
project, err = api.GetProject(projectName, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get project: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
|
||||
FormatFlag := viper.GetString("output-format")
|
||||
if FormatFlag != "" {
|
||||
err = utils.PrintFormat(project, FormatFlag)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
view.ViewProjects(project.Payload)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package quota
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func Quota() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "quota",
|
||||
Short: "Manage quotas",
|
||||
Long: `Manage quotas of projects`,
|
||||
Example: ` harbor quota list`,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
ListQuotaCommand(),
|
||||
ViewQuotaCommand(),
|
||||
UpdateQuotaCommand(),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package quota
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/quota/list"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Lists the Quotas specified for each project
|
||||
func ListQuotaCommand() *cobra.Command {
|
||||
var opts api.ListQuotaFlags
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "list quotas",
|
||||
Long: "list quotas specified for each project",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if opts.PageSize > 100 {
|
||||
log.Errorf("page size should be less than or equal to 100")
|
||||
return
|
||||
}
|
||||
|
||||
quota, err := api.ListQuota(opts)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get quota list: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
FormatFlag := viper.GetString("output-format")
|
||||
if FormatFlag != "" {
|
||||
err = utils.PrintFormat(quota, FormatFlag)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get quota list: %v", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
list.ListQuotas(quota.Payload)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.Int64VarP(&opts.Page, "page", "", 1, "Page number")
|
||||
flags.Int64VarP(&opts.PageSize, "page-size", "", 0, "Size of per page (use 0 to fetch all)")
|
||||
flags.StringVarP(&opts.Reference, "ref", "", "", "Reference type of quota")
|
||||
flags.StringVarP(&opts.ReferenceID, "refid", "", "", "Reference ID of quota")
|
||||
flags.StringVarP(
|
||||
&opts.Sort,
|
||||
"sort",
|
||||
"",
|
||||
"",
|
||||
"Sort the resource list in ascending or descending order",
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package quota
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/quota/update"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type QuotaUpdateReq struct {
|
||||
// The new hard limits for the quota
|
||||
Hard ResourceList `json:"hard,omitempty"`
|
||||
}
|
||||
|
||||
type ResourceList map[string]int64
|
||||
|
||||
// UpdateQuotaCommand updates the quota
|
||||
func UpdateQuotaCommand() *cobra.Command {
|
||||
var (
|
||||
storage string
|
||||
)
|
||||
|
||||
var opts api.ListQuotaFlags
|
||||
cmd := &cobra.Command{
|
||||
Use: "update [QuotaID]",
|
||||
Short: "update quotas for projects",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
var storageValue int64
|
||||
|
||||
// get quota id with quota
|
||||
quota, err := GetQuotaFromUser(args, opts)
|
||||
if err != nil {
|
||||
log.Errorf("error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if storage != "" {
|
||||
if storage == "-1" {
|
||||
storageValue = -1
|
||||
} else {
|
||||
storageValue, err = utils.StorageStringToBytes(storage)
|
||||
if err != nil {
|
||||
log.Errorf("failed to parse storage: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
storage = update.UpdateQuotaView(quota)
|
||||
storageValue, err = utils.StorageStringToBytes(storage)
|
||||
if err != nil {
|
||||
log.Errorf("failed to parse storage: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
hardlimit := &models.QuotaUpdateReq{
|
||||
Hard: models.ResourceList{"storage": storageValue},
|
||||
}
|
||||
|
||||
err = api.UpdateQuota(quota.ID, hardlimit)
|
||||
if err != nil {
|
||||
log.Errorf("failed to update quota: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
log.Infof("quota updated successfully!")
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&storage, "storage", "", "", "Enter storage size (e.g., 50GiB, 20MiB, 4TiB)")
|
||||
flags.StringVarP(&opts.Reference, "project-name", "", "", "Get quota by project-name")
|
||||
flags.StringVarP(&opts.ReferenceID, "project-id", "", "", "Get quota by project ID")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func GetQuotaFromUser(args []string, opts api.ListQuotaFlags) (*models.Quota, error) {
|
||||
var err error
|
||||
var quota *models.Quota
|
||||
|
||||
if len(args) > 0 {
|
||||
quotaID, err := strconv.ParseInt(args[0], 10, 64)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("failed to parse quotaID: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
quota, err = api.GetQuota(int64(quotaID))
|
||||
if err != nil {
|
||||
err := fmt.Errorf("failed to get Quota: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
} else if opts.Reference != "" {
|
||||
project, err := api.GetProject(opts.Reference, false)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("failed to get project: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
projectID := project.Payload.ProjectID
|
||||
quota, err = api.GetQuotaByRef(int64(projectID))
|
||||
if err != nil {
|
||||
err := fmt.Errorf("failed to get quota: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
} else if opts.ReferenceID != "" {
|
||||
projectID, err := strconv.ParseInt(opts.ReferenceID, 10, 64)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("invalid projectID: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
quota, err = api.GetQuotaByRef(projectID)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("failed to get quota: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
quotaID := prompt.GetQuotaIDFromUser()
|
||||
if quotaID == 0 {
|
||||
err := fmt.Errorf("failed to get quotaID from user")
|
||||
return nil, err
|
||||
}
|
||||
quota, err = api.GetQuota(quotaID)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("failed to get quota: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return quota, nil
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package quota
|
||||
|
||||
import (
|
||||
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/quota/list"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// View a specified quota
|
||||
func ViewQuotaCommand() *cobra.Command {
|
||||
var opts api.ListQuotaFlags
|
||||
cmd := &cobra.Command{
|
||||
Use: "view [quotaID]",
|
||||
Short: "get quota by quota ID",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
var quota *models.Quota
|
||||
|
||||
// get quota id with quota
|
||||
quota, err = GetQuotaFromUser(args, opts)
|
||||
if err != nil {
|
||||
log.Errorf("error: %v", err)
|
||||
return
|
||||
}
|
||||
quotas := []*models.Quota{quota}
|
||||
FormatFlag := viper.GetString("output-format")
|
||||
if FormatFlag != "" {
|
||||
err = utils.PrintFormat(quota, FormatFlag)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get quota list: %v", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
list.ListQuotas(quotas)
|
||||
}
|
||||
},
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opts.Reference, "project-name", "", "", "Get quota by project-name")
|
||||
flags.StringVarP(&opts.ReferenceID, "project-id", "", "", "Get quota by project ID")
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -29,14 +29,22 @@ func RepoDeleteCmd() *cobra.Command {
|
|||
Long: `Delete a repository within a project in Harbor`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
var projectName string
|
||||
var repoName string
|
||||
if len(args) > 0 {
|
||||
projectName, repoName := utils.ParseProjectRepo(args[0])
|
||||
err = api.RepoDelete(projectName, repoName, false)
|
||||
projectName, repoName, err = utils.ParseProjectRepo(args[0])
|
||||
if err != nil {
|
||||
log.Errorf("failed to parse project/repo: %v", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
projectName := prompt.GetProjectNameFromUser()
|
||||
repoName := prompt.GetRepoNameFromUser(projectName)
|
||||
err = api.RepoDelete(projectName, repoName, false)
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
repoName = prompt.GetRepoNameFromUser(projectName)
|
||||
}
|
||||
err = api.RepoDelete(projectName, repoName, false)
|
||||
if err != nil {
|
||||
log.Errorf("failed to delete repository: %v", err)
|
||||
}
|
||||
|
|
|
@ -46,10 +46,13 @@ func ListRepositoryCommand() *cobra.Command {
|
|||
if len(args) > 0 {
|
||||
projectName = args[0]
|
||||
} else {
|
||||
projectName = prompt.GetProjectNameFromUser()
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
}
|
||||
|
||||
repos, err = api.ListRepository(projectName, false)
|
||||
repos, err = api.ListRepository(projectName, false, opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list repositories: %v", err)
|
||||
}
|
||||
|
|
|
@ -36,14 +36,20 @@ func RepoViewCmd() *cobra.Command {
|
|||
var repo *repository.GetRepositoryOK
|
||||
|
||||
if len(args) > 0 {
|
||||
projectName, repoName = utils.ParseProjectRepo(args[0])
|
||||
projectName, repoName, err = utils.ParseProjectRepo(args[0])
|
||||
if err != nil {
|
||||
log.Errorf("failed to parse project/repo: %v", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
projectName = prompt.GetProjectNameFromUser()
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
repoName = prompt.GetRepoNameFromUser(projectName)
|
||||
}
|
||||
|
||||
repo, err = api.RepoView(projectName, repoName)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("failed to get repository information: %v", err)
|
||||
return
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package scanner
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
func Scanner() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "scanner",
|
||||
Short: "scanner commands",
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
CreateScannerCommand(),
|
||||
ListScannerCommand(),
|
||||
ViewCommand(),
|
||||
MetadataCommand(),
|
||||
SetDefaultCommand(),
|
||||
DeleteCommand(),
|
||||
UpdateCommand(),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/scanner/create"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func CreateScannerCommand() *cobra.Command {
|
||||
var opts create.CreateView
|
||||
var ping bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create a scanner",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if opts.Name == "" || opts.Auth == "" || opts.URL == "" {
|
||||
create.CreateScannerView(&opts)
|
||||
}
|
||||
|
||||
if ping {
|
||||
err := api.PingScanner(opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to ping the scanner adapter: %v", err)
|
||||
}
|
||||
} else {
|
||||
err := api.CreateScanner(opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create scanner: %v", err.Error())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVar(&opts.Name, "name", "", "New name for the scanner")
|
||||
flags.StringVar(&opts.Description, "description", "", "New description for the scanner")
|
||||
flags.StringVar(&opts.Auth, "auth", "", "Authentication method [None|Basic|Bearer|X-ScannerAdapter-API-Key]")
|
||||
flags.StringVar(&opts.AccessCredential, "credential", "", "Authorization header for the Scanner Adapter API")
|
||||
flags.StringVar(&opts.URL, "url", "", "Base URL of the scanner adapter")
|
||||
flags.BoolVar(&opts.Disabled, "disabled", false, "Disable the scanner registration")
|
||||
flags.BoolVar(&opts.SkipCertVerify, "skip-cert-verification", false, "Skip certificate verification in HTTP requests")
|
||||
flags.BoolVar(&opts.UseInternalAddr, "use-internal-addr", false, "Use internal registry address for scanning")
|
||||
flags.BoolVarP(&ping, "ping", "", false, "Ping the scanner adapter without creating it.")
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func DeleteCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete [scanner-name]",
|
||||
Short: "Delete a scanner registration",
|
||||
Long: `Delete a scanner registration from Harbor.
|
||||
|
||||
You can:
|
||||
- Provide the scanner name as an argument to delete it directly, or
|
||||
- Omit the argument to select a scanner interactively.
|
||||
|
||||
Note: Deleting a scanner will permanently remove its registration and associated metadata from the system.
|
||||
|
||||
Examples:
|
||||
# Delete a scanner by name
|
||||
harbor scanner delete trivy-scanner
|
||||
|
||||
# Interactively choose a scanner to delete
|
||||
harbor scanner delete`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
var registrationID string
|
||||
if len(args) > 0 {
|
||||
scanner, err := api.GetScannerByName(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to retrieve scanner by name %q: %v", args[0], err)
|
||||
}
|
||||
registrationID = scanner.UUID
|
||||
} else {
|
||||
registrationID = prompt.GetScannerIdFromUser()
|
||||
}
|
||||
|
||||
err = api.DeleteScanner(registrationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete scanner: %v", err)
|
||||
}
|
||||
log.Infof("Scanner deleted successfully")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
list "github.com/goharbor/harbor-cli/pkg/views/scanner/list"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func ListScannerCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List scanners",
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
scannersResp, err := api.ListScanners()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list scanners: %v", err)
|
||||
}
|
||||
|
||||
scanners := scannersResp.Payload
|
||||
if len(scanners) == 0 {
|
||||
log.Info("No scanners found")
|
||||
return nil
|
||||
}
|
||||
|
||||
formatFlag := viper.GetString("output-format")
|
||||
if formatFlag != "" {
|
||||
err = utils.PrintFormat(scanners, formatFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
list.ListScanners(scanners)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/scanner/metadata"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func MetadataCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "metadata [scanner-name]",
|
||||
Short: "Retrieve metadata for a specific scanner",
|
||||
Long: `Retrieve detailed metadata for a specified scanner integration in Harbor.
|
||||
|
||||
You can either:
|
||||
- Provide the scanner name as an argument (recommended), or
|
||||
- Leave it blank to be prompted interactively.
|
||||
|
||||
The metadata includes supported MIME types, capabilities, vendor information, and more.
|
||||
|
||||
Examples:
|
||||
# Get metadata for a specific scanner by name
|
||||
harbor scanner metadata trivy-scanner
|
||||
|
||||
# Interactively select a scanner if no name is provided
|
||||
harbor scanner metadata
|
||||
|
||||
Flags:
|
||||
--output-format <format> Output format: 'json' or 'yaml' (default is table view)`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var registrationID string
|
||||
if len(args) > 0 {
|
||||
scanner, err := api.GetScannerByName(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to retrieve scanner by name %q: %v", args[0], err)
|
||||
}
|
||||
registrationID = scanner.UUID
|
||||
} else {
|
||||
registrationID = prompt.GetScannerIdFromUser()
|
||||
}
|
||||
|
||||
meta, err := api.GetScannerMetadata(registrationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get scanner metadata: %v", err)
|
||||
}
|
||||
|
||||
formatFlag := viper.GetString("output-format")
|
||||
if formatFlag != "" {
|
||||
err = utils.PrintFormat(meta, formatFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
metadata.DisplayScannerMetadata(meta.Payload)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func SetDefaultCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "set-default",
|
||||
Short: "Set the default scanner for Harbor",
|
||||
Long: `Set the scanner that will be used as the default in Harbor. This scanner will be used for all default scanning tasks unless another scanner is specified.`,
|
||||
Aliases: []string{"sd"},
|
||||
Example: `harbor scanner set-default [scanner-name]
|
||||
OR
|
||||
harbor scanner set-default --id <scanner-id>`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
var registrationID string
|
||||
if len(args) > 0 {
|
||||
scanner, err := api.GetScannerByName(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to retrieve scanner by name %q: %v", args[0], err)
|
||||
}
|
||||
registrationID = scanner.UUID
|
||||
} else {
|
||||
registrationID = prompt.GetScannerIdFromUser()
|
||||
}
|
||||
err = api.SetDefaultScanner(registrationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set default scanner: %v", err)
|
||||
}
|
||||
fmt.Printf("Scanner %q successfully set as the default.\n", registrationID)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/scanner/create"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/scanner/update"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func UpdateCommand() *cobra.Command {
|
||||
var opts create.CreateView
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "update [scanner-name]",
|
||||
Short: "Update a scanner registration",
|
||||
Long: `Update the fields of an existing scanner registration.
|
||||
|
||||
You can pass the scanner name as an argument, or the CLI will prompt you to enter a scanner ID.
|
||||
Only the fields passed through flags will be updated; other fields will retain their existing values.`,
|
||||
Example: `
|
||||
# Update description and URL for a scanner named 'trivy-scanner'
|
||||
harbor scanner update trivy-scanner --description "Updated scanner" --url "http://trivy.local:8080"
|
||||
|
||||
# Change the authentication method and credential
|
||||
harbor scanner update trivy-scanner --auth Basic --credential "base64encodedAuth"
|
||||
|
||||
# Disable the scanner and rename it
|
||||
harbor scanner update trivy-scanner --name "trivy-secure" --disabled
|
||||
|
||||
# If no name is passed, you'll be prompted to enter a Scanner ID
|
||||
harbor scanner update --description "Updated via ID prompt"
|
||||
`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var registrationID string
|
||||
if len(args) > 0 {
|
||||
scanner, err := api.GetScannerByName(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to retrieve scanner by name %q: %v", args[0], err)
|
||||
}
|
||||
registrationID = scanner.UUID
|
||||
} else {
|
||||
registrationID = prompt.GetScannerIdFromUser()
|
||||
}
|
||||
|
||||
resp, err := api.GetScanner(registrationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("scanner not found with ID %q: %v", registrationID, err)
|
||||
}
|
||||
existing := resp.GetPayload()
|
||||
|
||||
updateView := &models.ScannerRegistration{
|
||||
Name: existing.Name,
|
||||
Description: existing.Description,
|
||||
Auth: existing.Auth,
|
||||
AccessCredential: existing.AccessCredential,
|
||||
URL: existing.URL,
|
||||
Disabled: existing.Disabled,
|
||||
SkipCertVerify: existing.SkipCertVerify,
|
||||
UseInternalAddr: existing.UseInternalAddr,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
if flags.Changed("name") {
|
||||
updateView.Name = opts.Name
|
||||
}
|
||||
if flags.Changed("description") {
|
||||
updateView.Description = opts.Description
|
||||
}
|
||||
if flags.Changed("auth") {
|
||||
updateView.Auth = opts.Auth
|
||||
}
|
||||
if flags.Changed("credential") {
|
||||
updateView.AccessCredential = opts.AccessCredential
|
||||
}
|
||||
if flags.Changed("url") {
|
||||
updateView.URL = strfmt.URI(opts.URL)
|
||||
}
|
||||
if flags.Changed("disabled") {
|
||||
updateView.Disabled = &opts.Disabled
|
||||
}
|
||||
if flags.Changed("skip-cert-verification") {
|
||||
updateView.SkipCertVerify = &opts.SkipCertVerify
|
||||
}
|
||||
if flags.Changed("use-internal-addr") {
|
||||
updateView.UseInternalAddr = &opts.UseInternalAddr
|
||||
}
|
||||
|
||||
update.UpdateScannerView(updateView)
|
||||
|
||||
err = api.UpdateScanner(registrationID, *updateView)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update scanner: %v", err)
|
||||
}
|
||||
|
||||
log.Infof("Scanner %q updated successfully", updateView.Name)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVar(&opts.Name, "name", "", "New name for the scanner")
|
||||
flags.StringVar(&opts.Description, "description", "", "New description for the scanner")
|
||||
flags.StringVar(&opts.Auth, "auth", "", "Authentication method [None|Basic|Bearer|X-ScannerAdapter-API-Key]")
|
||||
flags.StringVar(&opts.AccessCredential, "credential", "", "Authorization header for the Scanner Adapter API")
|
||||
flags.StringVar(&opts.URL, "url", "", "Base URL of the scanner adapter")
|
||||
flags.BoolVar(&opts.Disabled, "disabled", false, "Disable the scanner registration")
|
||||
flags.BoolVar(&opts.SkipCertVerify, "skip-cert-verification", false, "Skip certificate verification in HTTP requests")
|
||||
flags.BoolVar(&opts.UseInternalAddr, "use-internal-addr", false, "Use internal registry address for scanning")
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/scanner/view"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func ViewCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "view [scanner-name]",
|
||||
Short: "Display detailed information about a scanner registration",
|
||||
Long: `Display full details of a scanner registration in Harbor.
|
||||
|
||||
You can:
|
||||
- Provide a scanner name to view its details directly.
|
||||
- Omit the argument to select a scanner interactively by ID.
|
||||
|
||||
Supports custom output formats using the --output-format flag (e.g., json, yaml, table).
|
||||
|
||||
Examples:
|
||||
# View a specific scanner by name
|
||||
harbor scanner view trivy-scanner
|
||||
|
||||
# Interactively choose a scanner to view
|
||||
harbor scanner view
|
||||
|
||||
# View scanner in JSON format
|
||||
harbor scanner view trivy-scanner --output-format=json`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var scanner *models.ScannerRegistration
|
||||
if len(args) > 0 {
|
||||
resp, err := api.GetScannerByName(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get scanner by name %q: %v", args[0], err)
|
||||
}
|
||||
scanner = &resp
|
||||
} else {
|
||||
id := prompt.GetScannerIdFromUser()
|
||||
resp, err := api.GetScanner(id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get scanner by ID %q: %v", id, err)
|
||||
}
|
||||
scanner = resp.GetPayload()
|
||||
}
|
||||
|
||||
outputFormat := viper.GetString("output-format")
|
||||
if outputFormat != "" {
|
||||
if err := utils.PrintFormat(scanner, outputFormat); err != nil {
|
||||
return fmt.Errorf("failed to format output: %v", err)
|
||||
}
|
||||
} else {
|
||||
view.ViewScanner(scanner)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ package immutable
|
|||
import (
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/immutable/create"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
@ -32,6 +33,7 @@ func CreateImmutableCommand() *cobra.Command {
|
|||
Example: "harbor tag immutable create",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
var projectName string
|
||||
createView := &create.CreateView{
|
||||
ScopeSelectors: create.ImmutableSelector{
|
||||
Decoration: opts.ScopeSelectors.Decoration,
|
||||
|
@ -43,12 +45,15 @@ func CreateImmutableCommand() *cobra.Command {
|
|||
},
|
||||
}
|
||||
if len(args) > 0 {
|
||||
err = createImmutableView(createView, args[0])
|
||||
projectName = args[0]
|
||||
} else {
|
||||
projectName := prompt.GetProjectNameFromUser()
|
||||
err = createImmutableView(createView, projectName)
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
}
|
||||
|
||||
err = createImmutableView(createView, projectName)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create immutable tag rule: %v", err)
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ package immutable
|
|||
import (
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
@ -33,7 +34,10 @@ func DeleteImmutableCommand() *cobra.Command {
|
|||
projectName = args[0]
|
||||
immutableId = prompt.GetImmutableTagRule(args[0])
|
||||
} else {
|
||||
projectName = prompt.GetProjectNameFromUser()
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
immutableId = prompt.GetImmutableTagRule(projectName)
|
||||
}
|
||||
err = api.DeleteImmutable(projectName, immutableId)
|
||||
|
|
|
@ -44,18 +44,23 @@ You can specify the project name as an argument or, if omitted, you will be prom
|
|||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
var resp immutable.ListImmuRulesOK
|
||||
var projectName string
|
||||
|
||||
if len(args) > 0 {
|
||||
projectName := args[0]
|
||||
resp, err = api.ListImmutable(projectName)
|
||||
projectName = args[0]
|
||||
} else {
|
||||
projectName := prompt.GetProjectNameFromUser()
|
||||
resp, err = api.ListImmutable(projectName)
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp, err = api.ListImmutable(projectName)
|
||||
if err != nil {
|
||||
log.Errorf("failed to list immutablility rule: %v", err)
|
||||
}
|
||||
|
||||
FormatFlag := viper.GetString("output-format")
|
||||
if FormatFlag != "" {
|
||||
utils.PrintPayloadInJSONFormat(resp)
|
||||
|
|
|
@ -36,6 +36,10 @@ func ElevateUserCmd() *cobra.Command {
|
|||
log.Errorf("failed to get user id for '%s': %v", args[0], err)
|
||||
return
|
||||
}
|
||||
if userId == 0 {
|
||||
log.Errorf("User with name '%s' not found", args[0])
|
||||
return
|
||||
}
|
||||
} else {
|
||||
userId = prompt.GetUserIdFromUser()
|
||||
}
|
||||
|
@ -50,10 +54,12 @@ func ElevateUserCmd() *cobra.Command {
|
|||
}
|
||||
|
||||
err = api.ElevateUser(userId)
|
||||
if isUnauthorizedError(err) {
|
||||
log.Error("Permission denied: Admin privileges are required to execute this command.")
|
||||
} else {
|
||||
log.Errorf("failed to elevate user: %v", err)
|
||||
if err != nil {
|
||||
if isUnauthorizedError(err) {
|
||||
log.Error("Permission denied: Admin privileges are required to execute this command.")
|
||||
} else {
|
||||
log.Errorf("failed to elevate user: %v", err)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue