Compare commits
128 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 | |
|
5cdc50c0c2 | |
|
5686c8d178 | |
|
9abb9ca3ea | |
|
ac1983e382 | |
|
099f441455 | |
|
39f0af1bb5 | |
|
02199e073b | |
|
f51d045bc1 | |
|
cec019adfa | |
|
fca4399974 | |
|
398ee6fb2e | |
|
2e4329fc09 | |
|
3b47fcc101 | |
|
09b644f699 | |
|
33d95a5ef9 | |
|
8184c6f49f | |
|
716d406a35 | |
|
1d08f33a84 | |
|
baa69c6ad5 | |
|
e089ed7143 | |
|
44918b9d4f | |
|
651cfb4090 | |
|
4689267fa5 | |
|
33f0f36a0f | |
|
801dd8e973 | |
|
76100db25b | |
|
5f79d02271 | |
|
97f196196a | |
|
d4e3c6cbec | |
|
c018676326 | |
|
f31f1b7334 | |
|
2e4a4f4140 | |
|
f6fd361644 | |
|
3cff25fbb0 | |
|
524adda2ca | |
|
db9d24ea9b | |
|
bcc6fe16e8 | |
|
0f13e438b2 | |
|
0a17861f33 | |
|
c702fe9c33 | |
|
afd5683b3c | |
|
7f439409d7 | |
|
8dd8abb21c | |
|
1c421839b9 | |
|
ec6bb49184 | |
|
2dfc6c87f6 | |
|
e23da81cd4 | |
|
8a7a5a6e0d | |
|
b290223e76 | |
|
3c16b0c4c1 | |
|
2e06912d03 | |
|
e0e8a05ddc |
|
@ -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/)
|
|
@ -0,0 +1,50 @@
|
|||
module dagger/harbor-cli
|
||||
|
||||
go 1.24.4
|
||||
|
||||
require (
|
||||
github.com/99designs/gqlgen v0.17.74
|
||||
github.com/Khan/genqlient v0.8.1
|
||||
github.com/vektah/gqlparser/v2 v2.5.27
|
||||
go.opentelemetry.io/otel v1.35.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0
|
||||
go.opentelemetry.io/otel/log v0.8.0
|
||||
go.opentelemetry.io/otel/sdk v1.35.0
|
||||
go.opentelemetry.io/otel/sdk/log v0.8.0
|
||||
go.opentelemetry.io/otel/trace v1.35.0
|
||||
go.opentelemetry.io/proto/otlp v1.3.1
|
||||
golang.org/x/sync v0.15.0
|
||||
google.golang.org/grpc v1.73.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 // indirect
|
||||
github.com/sosodev/duration v1.3.1 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
)
|
||||
|
||||
replace go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc => go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0
|
||||
|
||||
replace go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp => go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0
|
||||
|
||||
replace go.opentelemetry.io/otel/log => go.opentelemetry.io/otel/log v0.8.0
|
||||
|
||||
replace go.opentelemetry.io/otel/sdk/log => go.opentelemetry.io/otel/sdk/log v0.8.0
|
|
@ -0,0 +1,85 @@
|
|||
github.com/99designs/gqlgen v0.17.74 h1:1FuVtkXxOc87xpKio3f6sohREmec+Jvy86PcYOuwgWo=
|
||||
github.com/99designs/gqlgen v0.17.74/go.mod h1:a+iR6mfRLNRp++kDpooFHiPWYiWX3Yu1BIilQRHgh10=
|
||||
github.com/Khan/genqlient v0.8.1 h1:wtOCc8N9rNynRLXN3k3CnfzheCUNKBcvXmVv5zt6WCs=
|
||||
github.com/Khan/genqlient v0.8.1/go.mod h1:R2G6DzjBvCbhjsEajfRjbWdVglSH/73kSivC9TLWVjU=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN9coASF1GusYX6AlU=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0/go.mod h1:igFoXX2ELCW06bol23DWPB5BEWfZISOzSP5K2sbLea0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
|
||||
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=
|
||||
github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0/go.mod h1:5KXybFvPGds3QinJWQT7pmXf+TN5YIa7CNYObWRkj50=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 h1:9kV11HXBHZAvuPUZxmMWrH8hZn/6UnHX4K0mu36vNsU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0/go.mod h1:JyA0FHXe22E1NeNiHmVp7kFHglnexDQ7uRWDiiJ1hKQ=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI=
|
||||
go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk=
|
||||
go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs=
|
||||
go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
|
||||
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@ -1,3 +1,16 @@
|
|||
// 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 main
|
||||
|
||||
import (
|
||||
|
@ -6,13 +19,13 @@ import (
|
|||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
GOLANGCILINT_VERSION = "v1.61.0"
|
||||
GO_VERSION = "1.22.5"
|
||||
SYFT_VERSION = "v1.9.0"
|
||||
GORELEASER_VERSION = "v2.3.2"
|
||||
GOLANGCILINT_VERSION = "v2.1.2"
|
||||
GO_VERSION = "1.24.4"
|
||||
GORELEASER_VERSION = "v2.8.2"
|
||||
)
|
||||
|
||||
func New(
|
||||
|
@ -34,16 +47,14 @@ func (m *HarborCli) BuildDev(
|
|||
ctx context.Context,
|
||||
platform string,
|
||||
) *dagger.File {
|
||||
|
||||
fmt.Println("🛠️ Building Harbor-Cli with Dagger...")
|
||||
// Define the path for the binary output
|
||||
os, arch, err := parsePlatform(platform)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error parsing platform: %v", err)
|
||||
}
|
||||
builder := dag.Container().
|
||||
From("golang:"+GO_VERSION+"-alpine").
|
||||
From("golang:"+GO_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)).
|
||||
|
@ -51,9 +62,19 @@ func (m *HarborCli) BuildDev(
|
|||
WithMountedDirectory("/src", m.Source). // Ensure the source directory with go.mod is mounted
|
||||
WithWorkdir("/src").
|
||||
WithEnvVariable("GOOS", os).
|
||||
WithEnvVariable("GOARCH", arch).
|
||||
WithExec([]string{"go", "build", "-o", "bin/harbor-cli", "cmd/harbor/main.go"})
|
||||
return builder.File("bin/harbor-cli")
|
||||
WithEnvVariable("GOARCH", arch)
|
||||
|
||||
gitCommit, _ := builder.WithExec([]string{"git", "rev-parse", "--short", "HEAD", "--always"}).Stdout(ctx)
|
||||
buildTime := time.Now().UTC().Format(time.RFC3339)
|
||||
ldflagsArgs := fmt.Sprintf(`-X github.com/goharbor/harbor-cli/cmd/harbor/internal/version.Version=dev
|
||||
-X github.com/goharbor/harbor-cli/cmd/harbor/internal/version.GoVersion=%s
|
||||
-X github.com/goharbor/harbor-cli/cmd/harbor/internal/version.BuildTime=%s
|
||||
-X github.com/goharbor/harbor-cli/cmd/harbor/internal/version.GitCommit=%s
|
||||
`, GO_VERSION, buildTime, gitCommit)
|
||||
builder = builder.WithExec([]string{
|
||||
"go", "build", "-ldflags", ldflagsArgs, "-o", "/bin/harbor-cli", "/src/cmd/harbor/main.go",
|
||||
})
|
||||
return builder.File("/bin/harbor-cli")
|
||||
}
|
||||
|
||||
// Return list of containers for list of oses and arches
|
||||
|
@ -64,12 +85,30 @@ func (m *HarborCli) BuildDev(
|
|||
// now so that no one calls this https://github.com/dagger/dagger/issues/8202#issuecomment-2317291483
|
||||
func (m *HarborCli) build(
|
||||
ctx context.Context,
|
||||
version string,
|
||||
) []*dagger.Container {
|
||||
var builds []*dagger.Container
|
||||
|
||||
fmt.Println("🛠️ Building with Dagger...")
|
||||
oses := []string{"linux", "darwin", "windows"}
|
||||
arches := []string{"amd64", "arm64"}
|
||||
|
||||
// temp container with git installed
|
||||
temp := dag.Container().
|
||||
From("alpine:latest").
|
||||
WithMountedDirectory("/src", m.Source).
|
||||
// --no-cache option is to avoid caching the apk package index
|
||||
WithExec([]string{"apk", "add", "--no-cache", "git"}).
|
||||
WithWorkdir("/src")
|
||||
|
||||
gitCommit, _ := temp.WithExec([]string{"git", "rev-parse", "--short", "HEAD", "--always"}).Stdout(ctx)
|
||||
buildTime := time.Now().UTC().Format(time.RFC3339)
|
||||
ldflagsArgs := fmt.Sprintf(`-X github.com/goharbor/harbor-cli/cmd/harbor/internal/version.Version=%s
|
||||
-X github.com/goharbor/harbor-cli/cmd/harbor/internal/version.GoVersion=%s
|
||||
-X github.com/goharbor/harbor-cli/cmd/harbor/internal/version.BuildTime=%s
|
||||
-X github.com/goharbor/harbor-cli/cmd/harbor/internal/version.GitCommit=%s
|
||||
`, version, GO_VERSION, buildTime, gitCommit)
|
||||
|
||||
for _, goos := range oses {
|
||||
for _, goarch := range arches {
|
||||
bin_path := fmt.Sprintf("build/%s/%s/", goos, goarch)
|
||||
|
@ -83,7 +122,7 @@ func (m *HarborCli) build(
|
|||
WithWorkdir("/src").
|
||||
WithEnvVariable("GOOS", goos).
|
||||
WithEnvVariable("GOARCH", goarch).
|
||||
WithExec([]string{"go", "build", "-o", bin_path + "harbor", "/src/cmd/harbor/main.go"}).
|
||||
WithExec([]string{"go", "build", "-ldflags", ldflagsArgs, "-o", bin_path + "harbor", "/src/cmd/harbor/main.go"}).
|
||||
WithWorkdir(bin_path).
|
||||
WithExec([]string{"ls"}).
|
||||
WithEntrypoint([]string{"./harbor"})
|
||||
|
@ -94,12 +133,14 @@ func (m *HarborCli) build(
|
|||
return builds
|
||||
}
|
||||
|
||||
// LintReport Executes the Linter and writes the linting results to a file golangci-lint-report.sarif
|
||||
// Executes Linter and writes results to a file golangci-lint.report
|
||||
func (m *HarborCli) LintReport(ctx context.Context) *dagger.File {
|
||||
report := "golangci-lint-report.sarif"
|
||||
return m.lint(ctx).WithExec([]string{"golangci-lint", "run",
|
||||
"--out-format", "sarif:" + report,
|
||||
"--issues-exit-code", "0"}).File(report)
|
||||
report := "golangci-lint.report"
|
||||
return m.lint(ctx).WithExec([]string{
|
||||
"golangci-lint", "run", "-v",
|
||||
"--output.tab.path=" + report,
|
||||
"--issues-exit-code", "0",
|
||||
}).File(report)
|
||||
}
|
||||
|
||||
// Lint Run the linter golangci-lint
|
||||
|
@ -107,7 +148,7 @@ func (m *HarborCli) Lint(ctx context.Context) (string, error) {
|
|||
return m.lint(ctx).WithExec([]string{"golangci-lint", "run"}).Stderr(ctx)
|
||||
}
|
||||
|
||||
func (m *HarborCli) lint(ctx context.Context) *dagger.Container {
|
||||
func (m *HarborCli) lint(_ context.Context) *dagger.Container {
|
||||
fmt.Println("👀 Running linter and printing results to file golangci-lint.txt.")
|
||||
linter := dag.Container().
|
||||
From("golangci/golangci-lint:"+GOLANGCILINT_VERSION+"-alpine").
|
||||
|
@ -125,8 +166,10 @@ func (m *HarborCli) PublishImage(
|
|||
// +optional
|
||||
// +default=["latest"]
|
||||
imageTags []string,
|
||||
registryPassword *dagger.Secret) []string {
|
||||
builders := m.build(ctx)
|
||||
registryPassword *dagger.Secret,
|
||||
) []string {
|
||||
version := getVersion(imageTags)
|
||||
builders := m.build(ctx, version)
|
||||
releaseImages := []*dagger.Container{}
|
||||
|
||||
for i, tag := range imageTags {
|
||||
|
@ -137,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")
|
||||
|
@ -147,8 +193,18 @@ func (m *HarborCli) PublishImage(
|
|||
|
||||
ctr := dag.Container(dagger.ContainerOpts{Platform: dagger.Platform(os + "/" + arch)}).
|
||||
From("alpine:latest").
|
||||
WithWorkdir("/").
|
||||
WithFile("/harbor", builder.File("./harbor")).
|
||||
WithEntrypoint([]string{"./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)
|
||||
}
|
||||
|
||||
|
@ -159,7 +215,6 @@ func (m *HarborCli) PublishImage(
|
|||
fmt.Sprintf("%s/%s/harbor-cli:%s", registry, "harbor-cli", imageTag),
|
||||
dagger.ContainerPublishOpts{PlatformVariants: releaseImages},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -172,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")
|
||||
}
|
||||
|
||||
|
@ -181,36 +236,31 @@ 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")
|
||||
}
|
||||
|
||||
// RunDoc Generate CLI Documentation with doc.go and return the directory containing the generated files
|
||||
// Generate CLI Documentation and return the directory containing the generated files
|
||||
func (m *HarborCli) RunDoc(ctx context.Context) *dagger.Directory {
|
||||
return dag.Container().
|
||||
From("golang:"+GO_VERSION+"-alpine").
|
||||
|
@ -221,10 +271,11 @@ func (m *HarborCli) RunDoc(ctx context.Context) *dagger.Directory {
|
|||
WithMountedDirectory("/src", m.Source).
|
||||
WithWorkdir("/src/doc").
|
||||
WithExec([]string{"go", "run", "doc.go"}).
|
||||
WithExec([]string{"go", "run", "./man-docs/man_doc.go"}).
|
||||
WithWorkdir("/src").Directory("/src/doc")
|
||||
}
|
||||
|
||||
// Test Executes Go tests and returns the directory containing the test results
|
||||
// Executes Go tests
|
||||
func (m *HarborCli) Test(ctx context.Context) (string, error) {
|
||||
test := dag.Container().
|
||||
From("golang:"+GO_VERSION+"-alpine").
|
||||
|
@ -238,6 +289,106 @@ func (m *HarborCli) Test(ctx context.Context) (string, error) {
|
|||
return test.Stdout(ctx)
|
||||
}
|
||||
|
||||
// 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().
|
||||
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", "--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, "/")
|
||||
|
@ -247,6 +398,15 @@ func parsePlatform(platform string) (string, string, error) {
|
|||
return parts[0], parts[1], nil
|
||||
}
|
||||
|
||||
func getVersion(tags []string) string {
|
||||
for _, tag := range tags {
|
||||
if strings.HasPrefix(tag, "v") {
|
||||
return tag
|
||||
}
|
||||
}
|
||||
return "latest"
|
||||
}
|
||||
|
||||
// PublishImageAndSign builds and publishes container images to a registry with a specific tags and then signs them using Cosign.
|
||||
func (m *HarborCli) PublishImageAndSign(
|
||||
ctx context.Context,
|
||||
|
@ -261,7 +421,6 @@ func (m *HarborCli) PublishImageAndSign(
|
|||
// +optional
|
||||
actionsIdTokenRequestUrl string,
|
||||
) (string, error) {
|
||||
|
||||
imageAddrs := m.PublishImage(ctx, registry, registryUsername, imageTags, registryPassword)
|
||||
_, err := m.Sign(
|
||||
ctx,
|
||||
|
@ -309,7 +468,8 @@ func (m *HarborCli) Sign(ctx context.Context,
|
|||
|
||||
return cosing_ctr.WithSecretVariable("REGISTRY_PASSWORD", registryPassword).
|
||||
WithExec([]string{"cosign", "env"}).
|
||||
WithExec([]string{"cosign", "sign", "--yes", "--recursive",
|
||||
WithExec([]string{
|
||||
"cosign", "sign", "--yes", "--recursive",
|
||||
"--registry-username", registryUsername,
|
||||
"--registry-password", registryPasswordPlain,
|
||||
imageAddr,
|
|
@ -7,8 +7,8 @@ on:
|
|||
- "v*.*.*"
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '*.md'
|
||||
- 'assets/**'
|
||||
- "*.md"
|
||||
- "assets/**"
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
|
@ -22,19 +22,100 @@ jobs:
|
|||
- name: Dagger Version
|
||||
uses: sagikazarmark/dagger-version-action@v0.0.1
|
||||
|
||||
- name: Generate Document
|
||||
uses: dagger/dagger-for-github@v7
|
||||
with:
|
||||
version: ${{ steps.dagger_version.outputs.version }}
|
||||
verb: call
|
||||
args: run-doc export --path=doc
|
||||
|
||||
- name: Check for changes
|
||||
run: |
|
||||
# Check if any docs have been modified
|
||||
changed_files=$(git ls-files --others --modified --deleted --exclude-standard)
|
||||
|
||||
# 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 file changes found."
|
||||
fi
|
||||
continue-on-error: false
|
||||
|
||||
- name: Run Dagger golangci-lint
|
||||
uses: dagger/dagger-for-github@v7
|
||||
with:
|
||||
version: ${{ steps.dagger_version.outputs.version }}
|
||||
verb: call
|
||||
args: lint-report export --path=golangci-lint-report.sarif
|
||||
args: lint-report export --path=golangci-lint.report
|
||||
|
||||
- uses: reviewdog/action-setup@v1
|
||||
- name: Run Reviewdog
|
||||
env:
|
||||
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Generate lint summary
|
||||
run: |
|
||||
reviewdog -f=sarif -name="Golang Linter Report" -reporter=github-check -filter-mode nofilter -fail-level any -tee < golangci-lint-report.sarif
|
||||
echo "<h2> 📝 Lint results</h2>" >> $GITHUB_STEP_SUMMARY
|
||||
cat golangci-lint.report >> $GITHUB_STEP_SUMMARY
|
||||
# Check if the lint report contains any content (error or issues)
|
||||
if [ -s golangci-lint.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
|
||||
|
||||
# - uses: reviewdog/action-setup@v1
|
||||
# - name: Run Reviewdog
|
||||
# env:
|
||||
# REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# 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
|
||||
|
@ -49,7 +130,39 @@ jobs:
|
|||
with:
|
||||
version: ${{ steps.dagger_version.outputs.version }}
|
||||
verb: call
|
||||
args: test
|
||||
args: test-report export --path=TestReport.json
|
||||
|
||||
- name: Summarize Tests
|
||||
uses: robherley/go-test-action@v0.6.0
|
||||
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
|
||||
|
@ -59,7 +172,6 @@ jobs:
|
|||
args: build-dev --platform linux/amd64 export --path=./harbor-dev
|
||||
|
||||
push-latest-images:
|
||||
if: github.event.pull_request == null && !startsWith(github.ref, 'refs/tags/v')
|
||||
needs:
|
||||
- lint
|
||||
- test-code
|
||||
|
@ -67,13 +179,20 @@ jobs:
|
|||
contents: read
|
||||
id-token: write
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- name: Print GitHub ref for debugging
|
||||
run: |
|
||||
echo "GitHub ref: $GITHUB_REF"
|
||||
|
||||
- name: Checkout repo
|
||||
if: github.event_name == 'push' && (github.ref == 'refs/heads/main')
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Publish and Sign Snapshot Image
|
||||
if: github.event_name == 'push' && (github.ref == 'refs/heads/main')
|
||||
uses: ./.github/actions/publish-and-sign
|
||||
with:
|
||||
IMAGE_TAGS: latest
|
||||
|
@ -82,9 +201,7 @@ jobs:
|
|||
REGISTRY_ADDRESS: ${{ vars.REGISTRY_ADDRESS }}
|
||||
REGISTRY_USERNAME: ${{ vars.REGISTRY_USERNAME }}
|
||||
|
||||
|
||||
publish-release:
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs:
|
||||
- lint
|
||||
- test-code
|
||||
|
@ -93,12 +210,32 @@ jobs:
|
|||
packages: write
|
||||
id-token: write
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/'))
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout repo
|
||||
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/'))
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Push images
|
||||
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/'))
|
||||
uses: ./.github/actions/publish-and-sign
|
||||
with:
|
||||
IMAGE_TAGS: latest, ${{ github.ref_name }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
REGISTRY_ADDRESS: ${{ vars.REGISTRY_ADDRESS }}
|
||||
REGISTRY_USERNAME: ${{ vars.REGISTRY_USERNAME }}
|
||||
|
||||
- name: Create Release
|
||||
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/'))
|
||||
uses: dagger/dagger-for-github@v7
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
@ -108,7 +245,7 @@ jobs:
|
|||
args: "release --github-token=env:GITHUB_TOKEN"
|
||||
|
||||
- name: Publish and Sign Tagged Image
|
||||
if: success()
|
||||
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/'))
|
||||
uses: ./.github/actions/publish-and-sign
|
||||
with:
|
||||
IMAGE_TAGS: "latest, ${{ github.ref_name }}"
|
||||
|
@ -116,4 +253,3 @@ jobs:
|
|||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
REGISTRY_ADDRESS: ${{ vars.REGISTRY_ADDRESS }}
|
||||
REGISTRY_USERNAME: ${{ vars.REGISTRY_USERNAME }}
|
||||
|
||||
|
|
|
@ -1,17 +1,47 @@
|
|||
run:
|
||||
timeout: 3m
|
||||
version: "2"
|
||||
linters:
|
||||
enable:
|
||||
# Default linters are already enabled, these are the additional ones
|
||||
- typecheck
|
||||
- gofmt
|
||||
- bodyclose
|
||||
- dupl
|
||||
- goheader
|
||||
- gosec
|
||||
- misspell
|
||||
- nilnil
|
||||
# - wrapcheck
|
||||
# - gocritic
|
||||
# - revive #, enable once current issues are resolved
|
||||
issues:
|
||||
exclude-dirs:
|
||||
- dagger/internal
|
||||
exclude-files:
|
||||
- ^.*\\.gen\\.go$
|
||||
- staticcheck
|
||||
- whitespace
|
||||
settings:
|
||||
goheader:
|
||||
template-path: copyright.tmpl
|
||||
misspell:
|
||||
locale: US,UK
|
||||
staticcheck:
|
||||
checks:
|
||||
- ST1019
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
paths:
|
||||
- ^.*\\.gen\\.go$
|
||||
- dagger/internal
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- goimports
|
||||
settings:
|
||||
gofmt:
|
||||
simplify: false
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- ^.*\\.gen\\.go$
|
||||
- dagger/internal
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
version: 2
|
||||
project_name: harbor
|
||||
project_name: harbor-cli
|
||||
|
||||
before:
|
||||
hooks:
|
||||
|
@ -11,6 +11,7 @@ builds:
|
|||
- CGO_ENABLED=0
|
||||
ldflags:
|
||||
- -w -s -X github.com/goharbor/harbor-cli/cmd/harbor/internal/version.GitCommit={{.FullCommit}}
|
||||
- -X github.com/goharbor/harbor-cli/cmd/harbor/internal/version.Version={{.Tag}}
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
|
@ -62,6 +63,22 @@ release:
|
|||
owner: goharbor # Your GitHub repository owner
|
||||
name: harbor-cli # Your GitHub repository name
|
||||
|
||||
# https://goreleaser.com/customization/homebrew/
|
||||
brews:
|
||||
- repository:
|
||||
owner: goharbor # GitHub user/org who owns the tap repo
|
||||
name: homebrew-tap # Tap repo name (i.e., goharbor/homebrew-tap)
|
||||
branch: main
|
||||
name: harbor-cli # Name of the CLI, becomes harbor-cli.rb
|
||||
commit_author: # Who commits to the tap repo
|
||||
name: goreleaserbot
|
||||
email: bot@goreleaser.com
|
||||
commit_msg_template: "Brew formula update for {{ .ProjectName }} version {{ .Tag }}"
|
||||
homepage: "https://goharbor.io"
|
||||
description: "Harbor CLI for interacting with Harbor registry" # Formula description
|
||||
test: |
|
||||
system "#{bin}/harbor-cli", "version" # Formula test (after install)
|
||||
|
||||
changelog:
|
||||
use: github
|
||||
filters:
|
||||
|
|
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. 🙌**
|
145
README.md
145
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,19 +117,26 @@ harbor help
|
|||
|
||||
|
||||
Available Commands:
|
||||
artifact Manage artifacts
|
||||
completion Generate the autocompletion script for the specified shell
|
||||
health Get the health status of Harbor components
|
||||
help Help about any command
|
||||
login Log in to Harbor registry
|
||||
project Manage projects and assign resources to them
|
||||
registry Manage registries
|
||||
repo Manage repositories
|
||||
user Manage users
|
||||
version Version of Harbor CLI
|
||||
artifact Manage artifacts
|
||||
completion Generate the autocompletion script for the specified shell
|
||||
config Manage the config of the Harbor CLI
|
||||
cve-allowlist Manage system CVE allowlist
|
||||
health Get the health status of Harbor components
|
||||
help Help about any command
|
||||
info Show the current credential information
|
||||
instance Manage preheat provider instances in Harbor
|
||||
label Manage labels in Harbor
|
||||
login Log in to Harbor registry
|
||||
project Manage projects and assign resources to them
|
||||
registry Manage registries
|
||||
repo Manage repositories
|
||||
schedule Schedule jobs in Harbor
|
||||
tag Manage tags in Harbor registry
|
||||
user Manage users
|
||||
version Version of Harbor CLI
|
||||
|
||||
Flags:
|
||||
--config string config file (default is $HOME/.harbor/config.yaml) (default "/Users/vadim/.harbor/config.yaml")
|
||||
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
|
||||
-h, --help help for harbor
|
||||
-o, --output-format string Output format. One of: json|yaml
|
||||
-v, --verbose verbose output
|
||||
|
@ -100,8 +146,36 @@ Use "harbor [command] --help" for more information about a command.
|
|||
|
||||
```
|
||||
|
||||
#### Config Management
|
||||
|
||||
##### Hierarchy
|
||||
Use the `--config` flag to specify a custom configuration file path (the highest priority).
|
||||
|
||||
```bash
|
||||
harbor --config /path/to/custom/config.yaml artifact list
|
||||
```
|
||||
|
||||
If `--config` is not provided, Harbor CLI checks the `HARBOR_CLI_CONFIG` environment variable for the config file path.
|
||||
|
||||
```bash
|
||||
export HARBOR_CLI_CONFIG=/path/to/custom/config.yaml
|
||||
harbor artifact list
|
||||
```
|
||||
|
||||
If neither is set, it defaults to `$XDG_CONFIG_HOME/harbor-cli/config.yaml` or `$HOME/.config/harbor-cli/config.yaml` if `XDG_CONFIG_HOME` is unset.
|
||||
```bash
|
||||
harbor artifact list
|
||||
```
|
||||
|
||||
##### Data Path
|
||||
- Data paths are determined by the `XDG_DATA_HOME` environment variable.
|
||||
- If `XDG_DATA_HOME` is not set, it defaults to `$HOME/.local/share/harbor-cli/data.yaml`.
|
||||
- The data file always contains the path of the latest config used.
|
||||
|
||||
##### Config TL;DR
|
||||
- `--config` flag > `HARBOR_CLI_CONFIG` environment variable > default XDG config paths.
|
||||
- Environment variables override default settings, and the `--config` flag takes precedence over both environment variables and defaults.
|
||||
- The data file always contains the path of the latest config used.
|
||||
|
||||
|
||||
#### Log in to Harbor Registry
|
||||
|
@ -154,9 +228,10 @@ Windows | ✅
|
|||
|
||||
# Build From Source
|
||||
|
||||
Make sure you have latest [Dagger](https://docs.dagger.io/) installed in your system.
|
||||
Make sure you have the latest [Dagger](https://docs.dagger.io/) installed in your system.
|
||||
|
||||
#### Using Dagger
|
||||
|
||||
```bash
|
||||
git clone https://github.com/goharbor/harbor-cli.git && cd harbor-cli
|
||||
dagger call build-dev --platform darwin/arm64 export --path=./harbor-cli
|
||||
|
@ -166,14 +241,14 @@ dagger call build-dev --platform darwin/arm64 export --path=./harbor-cli
|
|||
If golang is installed in your system, you can also build the project using the following commands:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/goharbor/harbor-cli.git
|
||||
git clone https://github.com/goharbor/harbor-cli.git && cd harbor-cli
|
||||
go build -o harbor-cli cmd/harbor/main.go
|
||||
```
|
||||
|
||||
# Version Compatibility With Harbor
|
||||
|
||||
At the moment, the Harbor CLI is developed and tested with Harbor 2.11.
|
||||
The CLI should work with versions prior to 2.11,
|
||||
At the moment, the Harbor CLI is developed and tested with Harbor 2.13.
|
||||
The CLI should work with versions prior to 2.13,
|
||||
but not all functionalities may be available or work as expected.
|
||||
|
||||
Harbor <2.0.0 is not supported.
|
||||
|
@ -191,6 +266,9 @@ Harbor <2.0.0 is not supported.
|
|||
|
||||
This project is licensed under the Apache 2.0 License. See the [LICENSE](https://github.com/goharbor/harbor-cli/blob/main/LICENSE) file for details.
|
||||
|
||||
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Fgoharbor%2Fharbor-cli?ref=badge_large)
|
||||
|
||||
# Acknowledgements
|
||||
|
||||
This project is maintained by the Harbor community. We thank all our contributors and users for their support.
|
||||
|
@ -199,3 +277,4 @@ This project is maintained by the Harbor community. We thank all our contributor
|
|||
|
||||
For any questions or issues, please open an issue on our [GitHub Issues](https://github.com/goharbor/harbor-cli/issues) page.<br>
|
||||
Give a ⭐ if this project helped you, Thank YOU!
|
||||
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
// 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 version
|
||||
|
||||
import "runtime/debug"
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
// 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 main
|
||||
|
||||
import (
|
||||
|
|
|
@ -1,11 +1,24 @@
|
|||
// 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 artifact
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/artifact/label"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func Artifact() *cobra.Command {
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "artifact",
|
||||
Short: "Manage artifacts",
|
||||
|
@ -19,6 +32,7 @@ func Artifact() *cobra.Command {
|
|||
DeleteArtifactCommand(),
|
||||
ScanArtifactCommand(),
|
||||
ArtifactTagsCmd(),
|
||||
label.LabelsArtifactCommmand(),
|
||||
)
|
||||
|
||||
return cmd
|
||||
|
|
|
@ -1,6 +1,21 @@
|
|||
// 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 artifact
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
|
@ -13,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
|
||||
}
|
|
@ -1,12 +1,26 @@
|
|||
// 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 artifact
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/go-client/pkg/sdk/v2.0/client/artifact"
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
artifactViews "github.com/goharbor/harbor-cli/pkg/views/artifact/list"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
@ -16,35 +30,55 @@ func ListArtifactCommand() *cobra.Command {
|
|||
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "list artifacts within a repository",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Short: "List container artifacts (images, charts, etc.) in a Harbor repository with metadata",
|
||||
Long: `List all artifacts (e.g., container images, charts) within a given Harbor repository.
|
||||
Supports optional project/repository input in the form <project>/<repository>.
|
||||
Displays key artifact metadata including tags, digest, type, size, vulnerability count, and push time.
|
||||
|
||||
Examples:
|
||||
harbor-cli artifact list # Interactive prompt for project and repository
|
||||
harbor-cli artifact list library/nginx # Directly list artifacts in the nginx repo under 'library' project
|
||||
|
||||
Supports pagination, search queries, and sorting using flags.`,
|
||||
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if opts.PageSize > 100 {
|
||||
return fmt.Errorf("page size should be less than or equal to 100")
|
||||
}
|
||||
var err error
|
||||
var artifacts artifact.ListArtifactsOK
|
||||
var projectName, repoName string
|
||||
|
||||
if len(args) > 0 {
|
||||
projectName, repoName = utils.ParseProjectRepo(args[0])
|
||||
projectName, repoName, err = utils.ParseProjectRepo(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse project/repo: %v", err)
|
||||
}
|
||||
} else {
|
||||
projectName = prompt.GetProjectNameFromUser()
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
repoName = prompt.GetRepoNameFromUser(projectName)
|
||||
}
|
||||
|
||||
artifacts, err = api.ListArtifact(projectName, repoName, opts)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("failed to list artifacts: %v", err)
|
||||
return fmt.Errorf("failed to list artifacts: %v", err)
|
||||
}
|
||||
|
||||
FormatFlag := viper.GetString("output-format")
|
||||
if FormatFlag != "" {
|
||||
err = utils.PrintFormat(artifacts, FormatFlag)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
artifactViews.ListArtifacts(artifacts.Payload)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,21 @@
|
|||
// 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 artifact
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
|
@ -31,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
|
||||
|
@ -59,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)
|
||||
}
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
// 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 artifact
|
||||
|
||||
import (
|
||||
|
@ -35,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)
|
||||
}
|
||||
|
@ -68,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)
|
||||
}
|
||||
|
||||
|
@ -92,7 +119,6 @@ func ListTagsCmd() *cobra.Command {
|
|||
} else {
|
||||
list.ListTags(tags.Payload)
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -106,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)
|
||||
}
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
// 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 artifact
|
||||
|
||||
import (
|
||||
|
@ -12,29 +25,44 @@ import (
|
|||
)
|
||||
|
||||
func ViewArtifactCommmand() *cobra.Command {
|
||||
|
||||
cmd := &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)
|
||||
return
|
||||
}
|
||||
|
||||
FormatFlag := viper.GetString("output-format")
|
||||
|
@ -47,7 +75,6 @@ func ViewArtifactCommmand() *cobra.Command {
|
|||
} else {
|
||||
view.ViewArtifact(artifact.Payload)
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -1,15 +1,42 @@
|
|||
// 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"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/artifact"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/context"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/cve"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/instance"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/labels"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/project"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/quota"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/registry"
|
||||
repositry "github.com/goharbor/harbor-cli/cmd/harbor/root/repository"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/replication"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/repository"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/robot"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/scan_all"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/scanner"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/schedule"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/tag"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/user"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/webhook"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
@ -39,6 +66,18 @@ harbor help
|
|||
// Initialize configuration
|
||||
utils.InitConfig(cfgFile, userSpecifiedConfig)
|
||||
|
||||
// Conditionally set the timestamp format only in verbose mode
|
||||
formatter := &logrus.TextFormatter{}
|
||||
|
||||
if verbose {
|
||||
formatter.FullTimestamp = true
|
||||
formatter.TimestampFormat = time.RFC3339
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
} else {
|
||||
logrus.SetOutput(io.Discard)
|
||||
}
|
||||
logrus.SetFormatter(formatter)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
@ -57,17 +96,102 @@ harbor help
|
|||
fmt.Println(err.Error())
|
||||
}
|
||||
|
||||
root.AddCommand(
|
||||
versionCommand(),
|
||||
LoginCommand(),
|
||||
project.Project(),
|
||||
registry.Registry(),
|
||||
repositry.Repository(),
|
||||
user.User(),
|
||||
artifact.Artifact(),
|
||||
HealthCommand(),
|
||||
schedule.Schedule(),
|
||||
)
|
||||
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
|
||||
}
|
|
@ -0,0 +1,417 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package context_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
helpers "github.com/goharbor/harbor-cli/test/helper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_ContextCmd(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"context"})
|
||||
err := rootCmd.Execute()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func Test_ContextListCmd(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"context", "list"})
|
||||
err := rootCmd.Execute()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func Test_ContextGetCmd_Success(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
testConfig := &utils.HarborConfig{
|
||||
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
|
||||
Credentials: []utils.Credential{
|
||||
{
|
||||
Name: "harbor-cli@http://demo.goharbor.io",
|
||||
ServerAddress: "http://demo.goharbor.io",
|
||||
Username: "harbor-cli",
|
||||
Password: "Harbor12345",
|
||||
},
|
||||
},
|
||||
}
|
||||
err := utils.UpdateConfigFile(testConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"context", "get", "credentials.serveraddress"})
|
||||
err = rootCmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_ContextGetCmd_Failure(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
testConfig := &utils.HarborConfig{
|
||||
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
|
||||
Credentials: []utils.Credential{
|
||||
{
|
||||
Name: "harbor-cli@http://demo.goharbor.io",
|
||||
ServerAddress: "http://demo.goharbor.io",
|
||||
Username: "harbor-cli",
|
||||
Password: "Harbor12345",
|
||||
},
|
||||
},
|
||||
}
|
||||
err := utils.UpdateConfigFile(testConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"context", "get", "serveraddress"})
|
||||
err = rootCmd.Execute()
|
||||
assert.Error(t, err, "Expected an error when getting a non-existent config item")
|
||||
}
|
||||
|
||||
func Test_ContextGetCmd_CredentialName_Success(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
testConfig := &utils.HarborConfig{
|
||||
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
|
||||
Credentials: []utils.Credential{
|
||||
{
|
||||
Name: "harbor-cli@http://demo.goharbor.io",
|
||||
ServerAddress: "http://demo.goharbor.io",
|
||||
Username: "harbor-cli",
|
||||
Password: "Harbor12345",
|
||||
},
|
||||
},
|
||||
}
|
||||
err := utils.UpdateConfigFile(testConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"context", "get", "credentials.serveraddress", "--name", "harbor-cli@http://demo.goharbor.io"})
|
||||
err = rootCmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_ContextGetCmd_CredentialName_Failure(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
testConfig := &utils.HarborConfig{
|
||||
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
|
||||
Credentials: []utils.Credential{
|
||||
{
|
||||
Name: "harbor-cli@http://demo.goharbor.io",
|
||||
ServerAddress: "http://demo.goharbor.io",
|
||||
Username: "harbor-cli",
|
||||
Password: "Harbor12345",
|
||||
},
|
||||
},
|
||||
}
|
||||
err := utils.UpdateConfigFile(testConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"context", "get", "credentials.serveraddress", "--name", "harbor-cli@http://goharbor.io"})
|
||||
err = rootCmd.Execute()
|
||||
assert.Error(t, err, "Expected an error when getting a non-existent credential name")
|
||||
}
|
||||
|
||||
func Test_ContextUpdateCmd_Success(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
testConfig := &utils.HarborConfig{
|
||||
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
|
||||
Credentials: []utils.Credential{
|
||||
{
|
||||
Name: "harbor-cli@http://demo.goharbor.io",
|
||||
ServerAddress: "http://demo.goharbor.io",
|
||||
Username: "harbor-cli",
|
||||
Password: "Harbor12345",
|
||||
},
|
||||
},
|
||||
}
|
||||
err := utils.UpdateConfigFile(testConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"context", "update", "credentials.serveraddress", "http://demo.goharbor.io"})
|
||||
err = rootCmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_ContextUpdateCmd_CredentialName_Success(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
testConfig := &utils.HarborConfig{
|
||||
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
|
||||
Credentials: []utils.Credential{
|
||||
{
|
||||
Name: "harbor-cli@http://demo.goharbor.io",
|
||||
ServerAddress: "http://demo.goharbor.io",
|
||||
Username: "harbor-cli",
|
||||
Password: "Harbor12345",
|
||||
},
|
||||
},
|
||||
}
|
||||
err := utils.UpdateConfigFile(testConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"context", "update", "credentials.serveraddress", "http://demo.goharbor.io", "--name", "harbor-cli@http://demo.goharbor.io"})
|
||||
err = rootCmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_ContextUpdateCmd_CredentialName_Failure(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
testConfig := &utils.HarborConfig{
|
||||
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
|
||||
Credentials: []utils.Credential{
|
||||
{
|
||||
Name: "harbor-cli@http://demo.goharbor.io",
|
||||
ServerAddress: "http://demo.goharbor.io",
|
||||
Username: "harbor-cli",
|
||||
Password: "Harbor12345",
|
||||
},
|
||||
},
|
||||
}
|
||||
err := utils.UpdateConfigFile(testConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"context", "update", "credentials.serveraddress", "http://demo.goharbor.io", "--name", "harbor-cli@http://goharbor.io"})
|
||||
err = rootCmd.Execute()
|
||||
assert.Error(t, err, "Expected an error when setting a non-existent credential name")
|
||||
}
|
||||
|
||||
func Test_ContextUpdateCmd_Failure(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
testConfig := &utils.HarborConfig{
|
||||
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
|
||||
Credentials: []utils.Credential{
|
||||
{
|
||||
Name: "harbor-cli@http://demo.goharbor.io",
|
||||
ServerAddress: "http://demo.goharbor.io",
|
||||
Username: "harbor-cli",
|
||||
Password: "Harbor12345",
|
||||
},
|
||||
},
|
||||
}
|
||||
err := utils.UpdateConfigFile(testConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"context", "update", "serveraddress", "http://demo.goharbor.io"})
|
||||
err = rootCmd.Execute()
|
||||
assert.Error(t, err, "Expected an error when setting a non-existent config item")
|
||||
}
|
||||
|
||||
func Test_ContextDeleteCmd_Success(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
testConfig := &utils.HarborConfig{
|
||||
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
|
||||
Credentials: []utils.Credential{
|
||||
{
|
||||
Name: "harbor-cli@http://demo.goharbor.io",
|
||||
ServerAddress: "http://demo.goharbor.io",
|
||||
Username: "harbor-cli",
|
||||
Password: "Harbor12345",
|
||||
},
|
||||
},
|
||||
}
|
||||
err := utils.UpdateConfigFile(testConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"context", "delete", "credentials.serveraddress"})
|
||||
err = rootCmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
config, err := utils.GetCurrentHarborConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Empty(t, config.Credentials[0].ServerAddress)
|
||||
}
|
||||
|
||||
func Test_ContextDeleteCmd_Failure(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
testConfig := &utils.HarborConfig{
|
||||
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
|
||||
Credentials: []utils.Credential{
|
||||
{
|
||||
Name: "harbor-cli@http://demo.goharbor.io",
|
||||
ServerAddress: "http://demo.goharbor.io",
|
||||
Username: "harbor-cli",
|
||||
Password: "Harbor12345",
|
||||
},
|
||||
},
|
||||
}
|
||||
err := utils.UpdateConfigFile(testConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"context", "delete", "serveraddress"})
|
||||
err = rootCmd.Execute()
|
||||
assert.Error(t, err, "Expected an error when deleting a non-existent config item")
|
||||
}
|
||||
|
||||
func Test_ContextDeleteCmd_CredentialName_Success(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
testConfig := &utils.HarborConfig{
|
||||
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
|
||||
Credentials: []utils.Credential{
|
||||
{
|
||||
Name: "harbor-cli@http://demo.goharbor.io",
|
||||
ServerAddress: "http://demo.goharbor.io",
|
||||
Username: "harbor-cli",
|
||||
Password: "Harbor12345",
|
||||
},
|
||||
},
|
||||
}
|
||||
err := utils.UpdateConfigFile(testConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"context", "delete", "credentials.serveraddress", "--name", "harbor-cli@http://demo.goharbor.io"})
|
||||
err = rootCmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
config, err := utils.GetCurrentHarborConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Empty(t, config.Credentials[0].ServerAddress)
|
||||
}
|
||||
|
||||
func Test_ContextDeleteCmd_CredentialName_Failure(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
testConfig := &utils.HarborConfig{
|
||||
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
|
||||
Credentials: []utils.Credential{
|
||||
{
|
||||
Name: "harbor-cli@http://demo.goharbor.io",
|
||||
ServerAddress: "http://demo.goharbor.io",
|
||||
Username: "harbor-cli",
|
||||
Password: "Harbor12345",
|
||||
},
|
||||
},
|
||||
}
|
||||
err := utils.UpdateConfigFile(testConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"context", "delete", "credentials.serveraddress", "--name", "harbor-cli@http://goharbor.io"})
|
||||
err = rootCmd.Execute()
|
||||
assert.Error(t, err, "Expected an error when deleting a non-existent credential name")
|
||||
}
|
||||
|
||||
func Test_ContextDeleteCmd_Current_Flag_Success(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
testConfig := &utils.HarborConfig{
|
||||
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
|
||||
Credentials: []utils.Credential{
|
||||
{
|
||||
Name: "harbor-cli@http://demo.goharbor.io",
|
||||
ServerAddress: "http://demo.goharbor.io",
|
||||
Username: "harbor-cli",
|
||||
Password: "Harbor12345",
|
||||
},
|
||||
{
|
||||
Name: "admin@http://demo.goharbor.io",
|
||||
ServerAddress: "http://demo.goharbor.io",
|
||||
Username: "admin",
|
||||
Password: "Admin12345",
|
||||
},
|
||||
},
|
||||
}
|
||||
err := utils.UpdateConfigFile(testConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"context", "delete", "--current"})
|
||||
err = rootCmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
config, err := utils.GetCurrentHarborConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Empty(t, config.CurrentCredentialName)
|
||||
assert.NotEmpty(t, config.Credentials)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_ContextDeleteCmd_Current_Flag_With_Item_Failure(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"context", "delete", "credentials.serveraddress", "--current"})
|
||||
err := rootCmd.Execute()
|
||||
assert.Error(t, err, "Expected an error when specifying both --current and an item")
|
||||
}
|
|
@ -0,0 +1,244 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var deleteCurrent bool
|
||||
|
||||
// DeleteContextItemCommand creates the 'harbor context delete' subcommand,
|
||||
// allowing you to do: harbor context delete <item>
|
||||
func DeleteContextItemCommand() *cobra.Command {
|
||||
var credentialName string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete <item>",
|
||||
Short: "Delete (clear) a specific config item",
|
||||
Example: `
|
||||
# Clear the current credential's password
|
||||
harbor context delete credentials.password
|
||||
|
||||
# Clear a specific credential's password using --name
|
||||
harbor context delete credentials.password --name admin@http://demo.goharbor.io
|
||||
|
||||
# Clear the current credential
|
||||
harbor context delete --current
|
||||
`,
|
||||
Long: `Clear the value of a specific CLI config item by setting it to its zero value.
|
||||
Case-insensitive field lookup, but uses the canonical (Go) field name internally.
|
||||
If you specify --name, that credential (rather than the "current" one) will be used.`,
|
||||
Args: cobra.RangeArgs(0, 1),
|
||||
|
||||
// Use RunE so we can propagate errors
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// 1. Load the current config
|
||||
config, err := utils.GetCurrentHarborConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load Harbor config: %w", err)
|
||||
}
|
||||
|
||||
// 1a. If --current is set, remove only the credential matching CurrentCredentialName
|
||||
if deleteCurrent {
|
||||
if len(args) > 0 {
|
||||
return fmt.Errorf("cannot specify both <item> and --current")
|
||||
}
|
||||
|
||||
currentName := config.CurrentCredentialName
|
||||
found := false
|
||||
if currentName != "" {
|
||||
for i, cred := range config.Credentials {
|
||||
if strings.EqualFold(cred.Name, currentName) {
|
||||
// Remove just this credential
|
||||
config.Credentials = append(config.Credentials[:i], config.Credentials[i+1:]...)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
config.CurrentCredentialName = ""
|
||||
}
|
||||
|
||||
if err := utils.UpdateConfigFile(config); err != nil {
|
||||
return fmt.Errorf("failed to save updated config: %w", err)
|
||||
}
|
||||
|
||||
if found {
|
||||
logrus.Infof("Removed credential '%s' and cleared CurrentCredentialName", currentName)
|
||||
} else {
|
||||
logrus.Infof("No credential named '%s' found; cleared CurrentCredentialName anyway", currentName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// If --all is NOT set, we'll perform the normal item-based delete.
|
||||
// Check we actually received an item (since now it's optional).
|
||||
if len(args) == 0 {
|
||||
return fmt.Errorf("please specify an <item> or use --current")
|
||||
}
|
||||
|
||||
// 2. Parse the user-supplied item path (e.g., "credentials.password")
|
||||
itemPath := strings.Split(args[0], ".")
|
||||
|
||||
// 3. Reflection-based delete (zero out)
|
||||
actualSegments := []string{}
|
||||
if err := deleteValueInConfig(config, itemPath, &actualSegments, credentialName); err != nil {
|
||||
return fmt.Errorf("failed to delete value in config: %w", err)
|
||||
}
|
||||
|
||||
// 4. Persist the updated config to disk
|
||||
if err := utils.UpdateConfigFile(config); err != nil {
|
||||
return fmt.Errorf("failed to save updated config: %w", err)
|
||||
}
|
||||
|
||||
// 5. Confirm to the user (no error here)
|
||||
canonicalPath := strings.Join(actualSegments, ".")
|
||||
logrus.Infof("Successfully cleared %s", canonicalPath)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Add --name / -n to let the user pick a specific credential
|
||||
cmd.Flags().StringVarP(
|
||||
&credentialName,
|
||||
"name",
|
||||
"n",
|
||||
"",
|
||||
"Name of the credential to delete fields from (default: the current credential)",
|
||||
)
|
||||
|
||||
cmd.Flags().BoolVar(
|
||||
&deleteCurrent,
|
||||
"current",
|
||||
false,
|
||||
"Remove current credentials from the config",
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// deleteValueInConfig checks whether the user is deleting something
|
||||
// under "credentials" (i.e., *a* credential) or a top-level field.
|
||||
//
|
||||
// If the user says "credentials.*" AND provides --name, we'll look
|
||||
// up that specific credential by name. Otherwise, we use CurrentCredentialName.
|
||||
func deleteValueInConfig(
|
||||
config *utils.HarborConfig,
|
||||
path []string,
|
||||
actualSegments *[]string,
|
||||
credentialName string,
|
||||
) error {
|
||||
if len(path) == 0 {
|
||||
return fmt.Errorf("no config item specified")
|
||||
}
|
||||
|
||||
// If the first segment is "credentials", pivot to the chosen credential.
|
||||
if strings.EqualFold(path[0], "credentials") {
|
||||
*actualSegments = append(*actualSegments, "Credentials")
|
||||
|
||||
// Figure out which credential name to use
|
||||
credName := config.CurrentCredentialName
|
||||
if credentialName != "" {
|
||||
credName = credentialName
|
||||
}
|
||||
|
||||
// Find the matching credential
|
||||
var targetCred *utils.Credential
|
||||
for i := range config.Credentials {
|
||||
if strings.EqualFold(config.Credentials[i].Name, credName) {
|
||||
targetCred = &config.Credentials[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if targetCred == nil {
|
||||
return fmt.Errorf("no matching credential found for '%s'", credName)
|
||||
}
|
||||
|
||||
// Remove "credentials" from path, delete the value in that credential
|
||||
return deleteNestedValue(targetCred, path[1:], actualSegments)
|
||||
}
|
||||
|
||||
// Otherwise, we delete a field in the main HarborConfig struct
|
||||
return deleteNestedValue(config, path, actualSegments)
|
||||
}
|
||||
|
||||
// deleteNestedValue navigates a pointer to a struct, following the path segments
|
||||
// in a case-insensitive manner, until the last segment, where it sets the field
|
||||
// to its zero value.
|
||||
func deleteNestedValue(obj interface{}, path []string, actualSegments *[]string) error {
|
||||
// We require obj to be a pointer to a struct so we can modify it.
|
||||
val := reflect.ValueOf(obj)
|
||||
if val.Kind() != reflect.Ptr {
|
||||
return fmt.Errorf("object must be a pointer to a struct, got %s", val.Kind())
|
||||
}
|
||||
val = val.Elem() // dereference pointer
|
||||
|
||||
for i, segment := range path {
|
||||
if val.Kind() != reflect.Struct {
|
||||
return fmt.Errorf("cannot traverse non-struct for segment '%s'", segment)
|
||||
}
|
||||
t := val.Type()
|
||||
|
||||
// Case-insensitive field lookup
|
||||
fieldIndex := -1
|
||||
for j := 0; j < val.NumField(); j++ {
|
||||
if strings.EqualFold(t.Field(j).Name, segment) {
|
||||
fieldIndex = j
|
||||
break
|
||||
}
|
||||
}
|
||||
if fieldIndex < 0 {
|
||||
return fmt.Errorf("config item '%s' does not exist", segment)
|
||||
}
|
||||
|
||||
field := t.Field(fieldIndex)
|
||||
fieldValue := val.Field(fieldIndex)
|
||||
|
||||
// Record the actual field name
|
||||
*actualSegments = append(*actualSegments, field.Name)
|
||||
|
||||
// If this is NOT the last path segment, move deeper
|
||||
if i < len(path)-1 {
|
||||
// If the field is a pointer and nil, we can't go deeper
|
||||
if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() {
|
||||
return fmt.Errorf("field '%s' is nil and cannot be traversed", field.Name)
|
||||
}
|
||||
// Descend
|
||||
val = fieldValue
|
||||
if val.Kind() == reflect.Ptr {
|
||||
val = val.Elem()
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// If this is the last segment, set the field to zero value
|
||||
if !fieldValue.CanSet() {
|
||||
return fmt.Errorf("cannot set field '%s' to zero value", field.Name)
|
||||
}
|
||||
|
||||
// The zero value for that field can be obtained with reflect.Zero().
|
||||
zeroVal := reflect.Zero(fieldValue.Type())
|
||||
fieldValue.Set(zeroVal)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package context
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// GetConfigItemCommand creates the 'harbor config get' subcommand.
|
||||
func GetContextItemCommand() *cobra.Command {
|
||||
var credentialName string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "get <item>",
|
||||
Short: "Get a specific config item",
|
||||
Example: `
|
||||
# Get the current credential's username
|
||||
harbor context get credentials.username
|
||||
|
||||
# Get a credential's username by specifying the credential name
|
||||
harbor config get credentials.username --name admin@http://demo.goharbor.io
|
||||
`,
|
||||
Long: `Get the value of a specific CLI config item.
|
||||
If you specify --name, that credential (rather than the "current" one) will be used.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// 1. Load config
|
||||
config, err := utils.GetCurrentHarborConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get config: %w", err)
|
||||
}
|
||||
|
||||
// 2. Parse the user-supplied item path (e.g., "credentials.username")
|
||||
itemPath := strings.Split(args[0], ".")
|
||||
|
||||
// 3. Get the value from the config (and track actual field segments for output)
|
||||
actualSegments := []string{}
|
||||
result, err := getValueFromConfig(config, itemPath, &actualSegments, credentialName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 4. Prepare the final output as a map for JSON/YAML rendering.
|
||||
canonicalPath := strings.Join(actualSegments, ".")
|
||||
output := map[string]interface{}{
|
||||
canonicalPath: result,
|
||||
}
|
||||
|
||||
// 5. Determine the output format (json, yaml, etc.) and print.
|
||||
formatFlag := viper.GetString("output-format")
|
||||
switch formatFlag {
|
||||
case "json":
|
||||
data, err := json.MarshalIndent(output, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal output to JSON: %w", err)
|
||||
}
|
||||
fmt.Println(string(data))
|
||||
|
||||
case "yaml", "":
|
||||
data, err := yaml.Marshal(output)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal output to YAML: %w", err)
|
||||
}
|
||||
fmt.Println(string(data))
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported output format: %s", formatFlag)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Add a --name / -n flag to allow specifying a credential
|
||||
cmd.Flags().StringVarP(
|
||||
&credentialName,
|
||||
"name",
|
||||
"n",
|
||||
"",
|
||||
"Name of the credential to get fields from (default: the current credential)",
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// getValueFromConfig decides if the user requested something under "credentials"
|
||||
// and if so, filters down to the *requested credential*, otherwise
|
||||
// it just searches in the top-level config object.
|
||||
func getValueFromConfig(
|
||||
config *utils.HarborConfig,
|
||||
path []string,
|
||||
actualSegments *[]string,
|
||||
credentialName string,
|
||||
) (interface{}, error) {
|
||||
if len(path) == 0 {
|
||||
return nil, fmt.Errorf("no config item specified")
|
||||
}
|
||||
|
||||
// If the first segment is "credentials", we pivot to a credential.
|
||||
if strings.EqualFold(path[0], "credentials") {
|
||||
*actualSegments = append(*actualSegments, "Credentials")
|
||||
|
||||
// Determine which credential name to use
|
||||
credName := config.CurrentCredentialName
|
||||
if credentialName != "" {
|
||||
credName = credentialName
|
||||
}
|
||||
|
||||
// Find the matching credential
|
||||
var targetCred *utils.Credential
|
||||
for i := range config.Credentials {
|
||||
if strings.EqualFold(config.Credentials[i].Name, credName) {
|
||||
targetCred = &config.Credentials[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if targetCred == nil {
|
||||
return nil, fmt.Errorf("no matching credential found for '%s'", credName)
|
||||
}
|
||||
|
||||
// Remove "credentials" from the path, keep the rest
|
||||
return getNestedValue(*targetCred, path[1:], actualSegments)
|
||||
}
|
||||
|
||||
// Otherwise, search in the overall config struct
|
||||
return getNestedValue(*config, path, actualSegments)
|
||||
}
|
||||
|
||||
// getNestedValue uses reflection to walk through struct fields
|
||||
// (case-insensitive) according to the provided path.
|
||||
//
|
||||
// 'actualSegments' is updated with the actual field names as we go.
|
||||
func getNestedValue(obj interface{}, path []string, actualSegments *[]string) (interface{}, error) {
|
||||
current := reflect.ValueOf(obj)
|
||||
|
||||
for _, key := range path {
|
||||
// If it's a pointer, dereference
|
||||
if current.Kind() == reflect.Ptr {
|
||||
current = current.Elem()
|
||||
}
|
||||
if current.Kind() != reflect.Struct {
|
||||
return nil, fmt.Errorf("cannot traverse non-struct for key '%s'", key)
|
||||
}
|
||||
|
||||
// Find the actual field by name, ignoring case
|
||||
var foundField reflect.StructField
|
||||
var fieldValue reflect.Value
|
||||
fieldFound := false
|
||||
|
||||
t := current.Type()
|
||||
for i := 0; i < current.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
if strings.EqualFold(field.Name, key) {
|
||||
foundField = field
|
||||
fieldValue = current.Field(i)
|
||||
fieldFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !fieldFound {
|
||||
return nil, fmt.Errorf("config item '%s' does not exist", key)
|
||||
}
|
||||
|
||||
// Record the *actual* field name in our slice
|
||||
*actualSegments = append(*actualSegments, foundField.Name)
|
||||
|
||||
// Descend for the next iteration
|
||||
current = fieldValue
|
||||
}
|
||||
|
||||
// Finally, if we ended on a pointer, dereference it
|
||||
if current.Kind() == reflect.Ptr {
|
||||
current = current.Elem()
|
||||
}
|
||||
return current.Interface(), nil
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/context/list"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func ListContextCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List contexts",
|
||||
Example: ` harbor context list`,
|
||||
Args: cobra.MaximumNArgs(0),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
config, err := utils.GetCurrentHarborConfig()
|
||||
if err != nil {
|
||||
fmt.Println("failed to get config: ", utils.ParseHarborErrorMsg(err))
|
||||
return
|
||||
}
|
||||
|
||||
// Get the output format
|
||||
formatFlag := viper.GetString("output-format")
|
||||
if formatFlag != "" {
|
||||
// Use utils.PrintFormat if available
|
||||
err = utils.PrintFormat(config, formatFlag)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to print config: ", utils.ParseHarborErrorMsg(err))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
var cxlist []api.ContextListView
|
||||
for _, cred := range config.Credentials {
|
||||
cx := api.ContextListView{Name: cred.Name, Username: cred.Username, Server: cred.ServerAddress}
|
||||
cxlist = append(cxlist, cx)
|
||||
}
|
||||
currentCredential := config.CurrentCredentialName
|
||||
list.ListContexts(cxlist, currentCredential)
|
||||
}
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,267 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// UpdateConfigItemCommand creates the 'harbor config update' subcommand,
|
||||
// allowing you to do: harbor config update <item> <value>.
|
||||
func UpdateContextItemCommand() *cobra.Command {
|
||||
var credentialName string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "update <item> <value>",
|
||||
Short: "Set/update a specific config item",
|
||||
Example: `
|
||||
# Set/update the current credential's password
|
||||
harbor context update credentials.password myNewSecret
|
||||
|
||||
# Set/update a credential's password by specifying the credential name
|
||||
harbor config update credentials.password myNewSecret --name admin@http://demo.goharbor.io
|
||||
`,
|
||||
Long: `Set/update the value of a specific CLI config item.
|
||||
Case-insensitive field lookup, but uses the canonical (Go) field name internally.
|
||||
If you specify --name, that credential (rather than the "current" one) will be updated.`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
|
||||
// Switch from Run to RunE so we can propagate errors
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// 1. Load the current config
|
||||
config, err := utils.GetCurrentHarborConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load Harbor config: %w", err)
|
||||
}
|
||||
|
||||
// 2. Parse the user-supplied item path (e.g., "credentials.password")
|
||||
itemPath := strings.Split(args[0], ".")
|
||||
newValue := args[1]
|
||||
|
||||
// 3. Reflection-based set
|
||||
actualSegments := []string{}
|
||||
if err := setValueInConfig(config, itemPath, newValue, &actualSegments, credentialName); err != nil {
|
||||
return fmt.Errorf("failed to set value in config: %w", err)
|
||||
}
|
||||
|
||||
// 4. Persist the updated config to disk
|
||||
if err := utils.UpdateConfigFile(config); err != nil {
|
||||
return fmt.Errorf("failed to save updated config: %w", err)
|
||||
}
|
||||
|
||||
// 5. Confirm to the user (logrus.Info is fine here; no error)
|
||||
canonicalPath := strings.Join(actualSegments, ".")
|
||||
logrus.Infof("Successfully updated %s to '%s'", canonicalPath, newValue)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Add a --name / -n flag to allow specifying a credential
|
||||
cmd.Flags().StringVarP(
|
||||
&credentialName,
|
||||
"name",
|
||||
"n",
|
||||
"",
|
||||
"Name of the credential to set fields on (default: the current credential)",
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// setValueInConfig checks whether the user is updating something
|
||||
// under "credentials" (i.e., a credential) or a top-level field.
|
||||
//
|
||||
// If path[0] == "credentials", we decide which credential to modify:
|
||||
// - If credentialName is non-empty, use that
|
||||
// - Otherwise, fallback to config.CurrentCredentialName
|
||||
func setValueInConfig(
|
||||
config *utils.HarborConfig,
|
||||
path []string,
|
||||
newValue string,
|
||||
actualSegments *[]string,
|
||||
credentialName string,
|
||||
) error {
|
||||
if len(path) == 0 {
|
||||
return fmt.Errorf("no config item specified")
|
||||
}
|
||||
|
||||
// If the first segment is "credentials", then we pivot to a specific credential.
|
||||
if strings.EqualFold(path[0], "credentials") {
|
||||
*actualSegments = append(*actualSegments, "Credentials")
|
||||
|
||||
// Determine which credential name to use
|
||||
credName := config.CurrentCredentialName
|
||||
if credentialName != "" {
|
||||
credName = credentialName
|
||||
}
|
||||
|
||||
// find the matching credential
|
||||
var matchingCred *utils.Credential
|
||||
for i := range config.Credentials {
|
||||
if strings.EqualFold(config.Credentials[i].Name, credName) {
|
||||
matchingCred = &config.Credentials[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if matchingCred == nil {
|
||||
return fmt.Errorf("no matching credential found for '%s'", credName)
|
||||
}
|
||||
|
||||
// Remove "credentials" from the path, and set the value in that credential
|
||||
return setNestedValue(matchingCred, path[1:], newValue, actualSegments)
|
||||
}
|
||||
|
||||
// Otherwise, we set a field in the main HarborConfig struct
|
||||
return setNestedValue(config, path, newValue, actualSegments)
|
||||
}
|
||||
|
||||
// setNestedValue navigates a pointer to a struct, following the path segments
|
||||
// in a case-insensitive manner, until the last segment, where it sets the value.
|
||||
//
|
||||
// If the last segment is Credentials.Password, it encrypts the user-supplied
|
||||
// password before storing it.
|
||||
func setNestedValue(obj interface{}, path []string, newValue string, actualSegments *[]string) error {
|
||||
// We require obj to be a pointer to a struct so we can modify it.
|
||||
val := reflect.ValueOf(obj)
|
||||
if val.Kind() != reflect.Ptr {
|
||||
return fmt.Errorf("object must be a pointer to a struct, got %s", val.Kind())
|
||||
}
|
||||
val = val.Elem() // dereference pointer
|
||||
|
||||
for i, segment := range path {
|
||||
if val.Kind() != reflect.Struct {
|
||||
return fmt.Errorf("cannot traverse non-struct for segment '%s'", segment)
|
||||
}
|
||||
t := val.Type()
|
||||
|
||||
// Case-insensitive field lookup
|
||||
fieldIndex := -1
|
||||
for j := 0; j < val.NumField(); j++ {
|
||||
if strings.EqualFold(t.Field(j).Name, segment) {
|
||||
fieldIndex = j
|
||||
break
|
||||
}
|
||||
}
|
||||
if fieldIndex < 0 {
|
||||
return fmt.Errorf("config item '%s' does not exist", segment)
|
||||
}
|
||||
|
||||
field := t.Field(fieldIndex)
|
||||
fieldValue := val.Field(fieldIndex)
|
||||
|
||||
// Record the actual field name
|
||||
*actualSegments = append(*actualSegments, field.Name)
|
||||
|
||||
// If this is NOT the last path segment, move deeper
|
||||
if i < len(path)-1 {
|
||||
// If the field is a pointer and nil, allocate a new instance
|
||||
if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() {
|
||||
newElem := reflect.New(fieldValue.Type().Elem())
|
||||
fieldValue.Set(newElem)
|
||||
}
|
||||
// Descend
|
||||
val = fieldValue
|
||||
if val.Kind() == reflect.Ptr {
|
||||
val = val.Elem()
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// If this is the last segment, set the value
|
||||
if !fieldValue.CanSet() {
|
||||
return fmt.Errorf("cannot set field '%s'", field.Name)
|
||||
}
|
||||
|
||||
switch fieldValue.Kind() {
|
||||
case reflect.String:
|
||||
// Special case: If we are setting Credentials.Password, encrypt it
|
||||
// We'll check the last two actual segments, e.g. ["Credentials", "Password"].
|
||||
if isCredentialsPassword(*actualSegments) {
|
||||
encrypted, err := encryptPassword(newValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fieldValue.SetString(encrypted)
|
||||
} else {
|
||||
fieldValue.SetString(newValue)
|
||||
}
|
||||
|
||||
case reflect.Bool:
|
||||
boolVal, err := strconv.ParseBool(newValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("field '%s' expects a bool, but got '%s'", field.Name, newValue)
|
||||
}
|
||||
fieldValue.SetBool(boolVal)
|
||||
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
intVal, err := strconv.ParseInt(newValue, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("field '%s' expects an integer, but got '%s'", field.Name, newValue)
|
||||
}
|
||||
fieldValue.SetInt(intVal)
|
||||
|
||||
// If you need to handle other types (e.g. float, slice), add them here.
|
||||
|
||||
default:
|
||||
return fmt.Errorf(
|
||||
"unsupported field type '%s' for field '%s'",
|
||||
fieldValue.Kind().String(), field.Name,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isCredentialsPassword checks if the actualSegments match ["Credentials", "Password"]
|
||||
// (case-insensitive).
|
||||
func isCredentialsPassword(actualSegments []string) bool {
|
||||
if len(actualSegments) < 2 {
|
||||
return false
|
||||
}
|
||||
// e.g. last two items might be Credentials, Password
|
||||
last := actualSegments[len(actualSegments)-1]
|
||||
secondLast := actualSegments[len(actualSegments)-2]
|
||||
return strings.EqualFold(secondLast, "Credentials") &&
|
||||
strings.EqualFold(last, "Password")
|
||||
}
|
||||
|
||||
// encryptPassword uses your existing utility functions to generate/retrieve a key
|
||||
// and return an encrypted version of the supplied password.
|
||||
func encryptPassword(plaintext string) (string, error) {
|
||||
// Make sure a key exists
|
||||
if err := utils.GenerateEncryptionKey(); err != nil {
|
||||
// It's okay if the key already exists; that might not be a fatal error for you
|
||||
logrus.Debugf("Encryption key might already exist: %v", err)
|
||||
}
|
||||
|
||||
key, err := utils.GetEncryptionKey()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get encryption key: %w", err)
|
||||
}
|
||||
|
||||
encrypted, err := utils.Encrypt(key, []byte(plaintext))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to encrypt password: %w", err)
|
||||
}
|
||||
return encrypted, nil
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -1,9 +1,24 @@
|
|||
// 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 (
|
||||
"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 {
|
||||
|
@ -11,16 +26,27 @@ func HealthCommand() *cobra.Command {
|
|||
Use: "health",
|
||||
Short: "Get the health status of Harbor components",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
err := api.Ping()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
status, err := api.GetHealth()
|
||||
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`,
|
||||
}
|
||||
|
||||
return cmd
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package root
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/internal/version"
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/info/list"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Lists the info of the Harbor system
|
||||
func InfoCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "info",
|
||||
Short: "Display detailed Harbor system, statistics, and CLI environment information",
|
||||
Long: `The 'info' command retrieves and displays general information about the Harbor instance,
|
||||
including system metadata, storage statistics, and CLI environment details such as user identity,
|
||||
registry address, and CLI version.
|
||||
|
||||
The output can be formatted as table (default), JSON, or YAML using the '--output-format' flag.`,
|
||||
Example: ` harbor info
|
||||
harbor info --output-format json
|
||||
harbor info -o yaml`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var cliinfo *api.CLIInfo
|
||||
var err error
|
||||
generalInfo, err := api.GetSystemInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stats, err := api.GetStats()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sysVolume, err := api.GetSystemVolumes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cliVersion := version.Version
|
||||
OSinfo := version.System
|
||||
|
||||
cliinfo, err = api.GetCLIInfo()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to get CLI info: %w", err)
|
||||
}
|
||||
systemInfo := list.CreateSystemInfo(
|
||||
generalInfo.Payload,
|
||||
stats.Payload,
|
||||
sysVolume.Payload,
|
||||
cliinfo,
|
||||
cliVersion,
|
||||
OSinfo,
|
||||
)
|
||||
|
||||
FormatFlag := viper.GetString("output-format")
|
||||
if FormatFlag != "" {
|
||||
err = utils.PrintFormat(systemInfo, FormatFlag)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
} else {
|
||||
list.ListInfo(&systemInfo)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
package labels
|
|
@ -1,3 +1,16 @@
|
|||
// 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 labels
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
@ -7,6 +20,12 @@ func Labels() *cobra.Command {
|
|||
Use: "label",
|
||||
Short: "Manage labels in Harbor",
|
||||
}
|
||||
cmd.AddCommand(
|
||||
CreateLabelCommand(),
|
||||
DeleteLabelCommand(),
|
||||
ListLabelCommand(),
|
||||
UpdateLableCommand(),
|
||||
)
|
||||
|
||||
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 labels
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/label/create"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func CreateLabelCommand() *cobra.Command {
|
||||
var opts create.CreateView
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "create label",
|
||||
Long: "create label in harbor",
|
||||
Example: "harbor label create",
|
||||
Args: cobra.ExactArgs(0),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
createView := &create.CreateView{
|
||||
Name: opts.Name,
|
||||
Color: opts.Color,
|
||||
Scope: opts.Scope,
|
||||
Description: opts.Description,
|
||||
}
|
||||
if opts.Name != "" && opts.Scope != "" {
|
||||
err = api.CreateLabel(opts)
|
||||
} else {
|
||||
err = createLabelView(createView)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("failed to create label: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opts.Name, "name", "n", "", "Name of the label")
|
||||
flags.StringVarP(&opts.Color, "color", "", "#FFFFFF", "Color of the label.color is in hex value")
|
||||
flags.StringVarP(&opts.Scope, "scope", "s", "g", "Scope of the label. eg- g(global), p(specific project)")
|
||||
flags.StringVarP(&opts.Description, "description", "d", "", "Description of the label")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func createLabelView(createView *create.CreateView) error {
|
||||
if createView == nil {
|
||||
createView = &create.CreateView{}
|
||||
}
|
||||
|
||||
create.CreateLabelView(createView)
|
||||
return api.CreateLabel(*createView)
|
||||
}
|
|
@ -1 +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 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"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func DeleteLabelCommand() *cobra.Command {
|
||||
var opts models.Label
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete",
|
||||
Short: "delete label",
|
||||
Example: "harbor label delete [labelname]",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
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])
|
||||
} else {
|
||||
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 {
|
||||
return fmt.Errorf("failed to delete label: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opts.Scope, "scope", "s", "g", "default(global).'p' for project labels.Query scope of the label")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
// 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 labels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/label/list"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func ListLabelCommand() *cobra.Command {
|
||||
var opts api.ListFlags
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "list labels",
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if opts.PageSize > 100 {
|
||||
return fmt.Errorf("page size should be less than or equal to 100")
|
||||
}
|
||||
|
||||
label, err := api.ListLabel(opts)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get label list: %v", err)
|
||||
}
|
||||
if len(label.Payload) == 0 {
|
||||
log.Info("No labels found")
|
||||
return nil
|
||||
}
|
||||
formatFlag := viper.GetString("output-format")
|
||||
if formatFlag != "" {
|
||||
err = utils.PrintFormat(label, formatFlag)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
} else {
|
||||
list.ListLabels(label.Payload)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.Int64VarP(&opts.Page, "page", "", 1, "Page number")
|
||||
flags.Int64VarP(&opts.PageSize, "page-size", "", 20, "Size of per page")
|
||||
flags.StringVarP(&opts.Q, "query", "q", "", "Query string to query resources")
|
||||
flags.StringVarP(&opts.Scope, "scope", "s", "g", "default(global).'p' for project labels.Query scope of the label")
|
||||
flags.Int64VarP(&opts.ProjectID, "projectid", "i", 1, "project ID when query project labels")
|
||||
flags.StringVarP(&opts.Sort, "sort", "", "", "Sort the label list in ascending or descending order")
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
// 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 labels
|
||||
|
||||
import (
|
||||
"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/label/update"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func UpdateLableCommand() *cobra.Command {
|
||||
opts := &models.Label{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "update",
|
||||
Short: "update label",
|
||||
Example: "harbor label update [labelname]",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
var labelId int64
|
||||
updateflags := api.ListFlags{
|
||||
Scope: opts.Scope,
|
||||
}
|
||||
|
||||
if len(args) > 0 {
|
||||
labelId, err = api.GetLabelIdByName(args[0])
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
|
||||
existingLabel := api.GetLabel(labelId)
|
||||
if existingLabel == nil {
|
||||
log.Errorf("label is not found")
|
||||
return
|
||||
}
|
||||
updateView := &models.Label{
|
||||
Name: existingLabel.Name,
|
||||
Color: existingLabel.Color,
|
||||
Description: existingLabel.Description,
|
||||
Scope: existingLabel.Scope,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
if flags.Changed("name") {
|
||||
updateView.Name = opts.Name
|
||||
}
|
||||
if flags.Changed("color") {
|
||||
updateView.Color = opts.Color
|
||||
}
|
||||
if flags.Changed("description") {
|
||||
updateView.Description = opts.Description
|
||||
}
|
||||
if flags.Changed("scope") {
|
||||
updateView.Scope = opts.Scope
|
||||
}
|
||||
|
||||
update.UpdateLabelView(updateView)
|
||||
err = api.UpdateLabel(updateView, labelId)
|
||||
if err != nil {
|
||||
log.Errorf("failed to update label: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opts.Name, "name", "n", "", "Name of the label")
|
||||
flags.StringVarP(&opts.Color, "color", "", "", "Color of the label.color is in hex value")
|
||||
flags.StringVarP(&opts.Scope, "scope", "s", "g", "Scope of the label. eg- g(global), p(specific project)")
|
||||
flags.StringVarP(&opts.Description, "description", "d", "", "Description of the label")
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -1,3 +1,16 @@
|
|||
// 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 (
|
||||
|
@ -51,28 +64,20 @@ func LoginCommand() *cobra.Command {
|
|||
Name: Name,
|
||||
}
|
||||
|
||||
// autogenerate name
|
||||
if loginView.Name == "" && loginView.Server != "" && loginView.Username != "" {
|
||||
loginView.Name = fmt.Sprintf("%s@%s", loginView.Username, utils.SanitizeServerAddress(loginView.Server))
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
if loginView.Server != "" && loginView.Username != "" && loginView.Password != "" {
|
||||
err = runLogin(loginView)
|
||||
} else {
|
||||
err = createLoginView(&loginView)
|
||||
}
|
||||
|
||||
var config *utils.HarborConfig
|
||||
config, err = utils.GetCurrentHarborConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to get current harbor config: %s", err)
|
||||
}
|
||||
if err := ProcessLogin(loginView, config); err != nil {
|
||||
return fmt.Errorf("login failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&Name, "name", "", "", "name for the set of credentials")
|
||||
flags.StringVarP(&Username, "username", "u", "", "Username")
|
||||
flags.StringVarP(&Password, "password", "p", "", "Password")
|
||||
flags.BoolVar(&passwordStdin, "password-stdin", false, "Take the password from stdin")
|
||||
|
@ -80,7 +85,23 @@ func LoginCommand() *cobra.Command {
|
|||
return cmd
|
||||
}
|
||||
|
||||
func createLoginView(loginView *login.LoginView) error {
|
||||
// ProcessLogin applies a simplified decision logic to run login or launch an interactive view.
|
||||
func ProcessLogin(loginView login.LoginView, config *utils.HarborConfig) error {
|
||||
// Auto-generate the name
|
||||
loginView.Name = fmt.Sprintf("%s@%s", loginView.Username, utils.SanitizeServerAddress(loginView.Server))
|
||||
|
||||
// If complete credentials are provided (overrides), run login using them directly.
|
||||
if loginView.Server != "" && loginView.Username != "" && loginView.Password != "" {
|
||||
return RunLogin(loginView)
|
||||
}
|
||||
|
||||
// If nothing matches, launch the interactive view.
|
||||
return CreateLoginView(&loginView)
|
||||
}
|
||||
|
||||
// CreateLoginView launches the interactive login view.
|
||||
// In this implementation, it calls login.CreateView and then tries to run login.
|
||||
func CreateLoginView(loginView *login.LoginView) error {
|
||||
if loginView == nil {
|
||||
loginView = &login.LoginView{
|
||||
Server: "",
|
||||
|
@ -91,10 +112,11 @@ func createLoginView(loginView *login.LoginView) error {
|
|||
}
|
||||
login.CreateView(loginView)
|
||||
|
||||
return runLogin(*loginView)
|
||||
return RunLogin(*loginView)
|
||||
}
|
||||
|
||||
func runLogin(opts login.LoginView) error {
|
||||
// RunLogin attempts to log in using the provided LoginView credentials.
|
||||
func RunLogin(opts login.LoginView) error {
|
||||
opts.Server = utils.FormatUrl(opts.Server)
|
||||
|
||||
clientConfig := &harbor.ClientSetConfig{
|
||||
|
@ -102,18 +124,36 @@ func runLogin(opts login.LoginView) error {
|
|||
Username: opts.Username,
|
||||
Password: opts.Password,
|
||||
}
|
||||
client := utils.GetClientByConfig(clientConfig)
|
||||
|
||||
ctx := context.Background()
|
||||
_, err := client.User.GetCurrentUserInfo(ctx, &user.GetCurrentUserInfoParams{})
|
||||
err := utils.ValidateURL(opts.Server)
|
||||
if err != nil {
|
||||
return fmt.Errorf("login failed, please check your credentials: %s", err)
|
||||
return fmt.Errorf("invalid server URL: %s", err)
|
||||
}
|
||||
client := utils.GetClientByConfig(clientConfig)
|
||||
ctx := context.Background()
|
||||
_, err = client.User.GetCurrentUserInfo(ctx, &user.GetCurrentUserInfoParams{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("%v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
if err := utils.GenerateEncryptionKey(); err != nil {
|
||||
fmt.Println("Encryption key already exists or could not be created:", err)
|
||||
}
|
||||
|
||||
key, err := utils.GetEncryptionKey()
|
||||
if err != nil {
|
||||
fmt.Println("Error getting encryption key:", err)
|
||||
return fmt.Errorf("failed to get encryption key: %s", err)
|
||||
}
|
||||
|
||||
encryptedPassword, err := utils.Encrypt(key, []byte(opts.Password))
|
||||
if err != nil {
|
||||
fmt.Println("Error encrypting password:", err)
|
||||
return fmt.Errorf("failed to encrypt password: %s", err)
|
||||
}
|
||||
|
||||
cred := utils.Credential{
|
||||
Name: opts.Name,
|
||||
Username: opts.Username,
|
||||
Password: opts.Password,
|
||||
Password: encryptedPassword,
|
||||
ServerAddress: opts.Server,
|
||||
}
|
||||
harborData, err := utils.GetCurrentHarborData()
|
||||
|
@ -125,14 +165,16 @@ func runLogin(opts login.LoginView) error {
|
|||
existingCred, err := utils.GetCredentials(opts.Name)
|
||||
if err == nil {
|
||||
if existingCred.Username == opts.Username && existingCred.ServerAddress == opts.Server {
|
||||
if existingCred.Password == opts.Password {
|
||||
if existingCred.Password == encryptedPassword {
|
||||
log.Warn("Credentials already exist in the config file. They were not added again.")
|
||||
fmt.Printf("Login successful for %s at %s\n", opts.Username, opts.Server)
|
||||
return nil
|
||||
} else {
|
||||
log.Warn("Credentials already exist in the config file but the password is different. Updating the password.")
|
||||
if err = utils.UpdateCredentialsInConfigFile(cred, configPath); err != nil {
|
||||
log.Fatalf("failed to update the credential: %s", err)
|
||||
}
|
||||
fmt.Printf("Login successful for %s at %s\n", opts.Username, opts.Server)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
|
@ -140,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
|
||||
}
|
||||
}
|
||||
|
@ -148,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
|
||||
}
|
||||
|
|
|
@ -1,23 +1,36 @@
|
|||
package e2e
|
||||
// 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_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root"
|
||||
helpers "github.com/goharbor/harbor-cli/test/helper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Login_Success(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := Initialize(t, tempDir)
|
||||
defer ConfigCleanup(t, data)
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
cmd := root.LoginCommand()
|
||||
validServerAddresses := []string{
|
||||
"http://demo.goharbor.io:80",
|
||||
"https://demo.goharbor.io:443",
|
||||
"http://demo.goharbor.io",
|
||||
"https://demo.goharbor.io",
|
||||
// "demo.goharbor.io",
|
||||
}
|
||||
|
||||
for _, serverAddress := range validServerAddresses {
|
||||
|
@ -25,7 +38,6 @@ func Test_Login_Success(t *testing.T) {
|
|||
args := []string{serverAddress}
|
||||
cmd.SetArgs(args)
|
||||
|
||||
assert.NoError(t, cmd.Flags().Set("name", "test"))
|
||||
assert.NoError(t, cmd.Flags().Set("username", "harbor-cli"))
|
||||
assert.NoError(t, cmd.Flags().Set("password", "Harbor12345"))
|
||||
|
||||
|
@ -37,13 +49,12 @@ func Test_Login_Success(t *testing.T) {
|
|||
|
||||
func Test_Login_Failure_WrongServer(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := Initialize(t, tempDir)
|
||||
defer ConfigCleanup(t, data)
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
|
||||
cmd := root.LoginCommand()
|
||||
cmd.SetArgs([]string{"wrongserver"})
|
||||
|
||||
assert.NoError(t, cmd.Flags().Set("name", "test"))
|
||||
assert.NoError(t, cmd.Flags().Set("username", "harbor-cli"))
|
||||
assert.NoError(t, cmd.Flags().Set("password", "Harbor12345"))
|
||||
|
||||
|
@ -53,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"))
|
||||
|
||||
|
@ -69,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
|
||||
}
|
|
@ -1,6 +1,20 @@
|
|||
// 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/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
@ -17,7 +31,9 @@ func Project() *cobra.Command {
|
|||
ListProjectCommand(),
|
||||
ViewCommand(),
|
||||
LogsProjectCommmand(),
|
||||
config.ProjectConfigCommand(),
|
||||
SearchProjectCommand(),
|
||||
Robot(),
|
||||
)
|
||||
|
||||
return cmd
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package config
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
var isID bool
|
||||
|
||||
func ProjectConfigCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Manage project configuration",
|
||||
}
|
||||
cmd.AddCommand(
|
||||
UpdateProjectConfigCmd(),
|
||||
ListProjectConfigCmd(),
|
||||
)
|
||||
cmd.PersistentFlags().BoolVarP(&isID, "id", "", false, "Use project ID instead of name")
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -1,7 +1,23 @@
|
|||
// 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 (
|
||||
"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"
|
||||
|
@ -15,33 +31,51 @@ func CreateProjectCommand() *cobra.Command {
|
|||
Use: "create [project name]",
|
||||
Short: "create project",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
createView := &create.CreateView{
|
||||
ProjectName: opts.ProjectName,
|
||||
Public: opts.Public,
|
||||
RegistryID: opts.RegistryID,
|
||||
StorageLimit: opts.StorageLimit,
|
||||
ProxyCache: false,
|
||||
}
|
||||
var ProjectName string
|
||||
if len(args) > 0 {
|
||||
opts.ProjectName = args[0]
|
||||
}
|
||||
|
||||
if opts.ProxyCache && opts.RegistryID == "" {
|
||||
return fmt.Errorf("proxy cache selected but no registry ID provided. Use --registry-id")
|
||||
}
|
||||
|
||||
if !opts.ProxyCache && opts.RegistryID != "" {
|
||||
return fmt.Errorf("registry ID should only be provided when proxy-cache is enabled")
|
||||
}
|
||||
|
||||
if opts.ProjectName != "" && opts.StorageLimit != "" {
|
||||
log.Debug("Attempting to create project using flags...")
|
||||
err = api.CreateProject(opts)
|
||||
ProjectName = opts.ProjectName
|
||||
} else {
|
||||
log.Debug("Switching to interactive view...")
|
||||
createView := &create.CreateView{
|
||||
ProjectName: opts.ProjectName,
|
||||
Public: opts.Public,
|
||||
RegistryID: opts.RegistryID,
|
||||
StorageLimit: opts.StorageLimit,
|
||||
ProxyCache: opts.ProxyCache,
|
||||
}
|
||||
|
||||
err = createProjectView(createView)
|
||||
ProjectName = createView.ProjectName
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("failed to create project: %v", err)
|
||||
return fmt.Errorf("failed to create project: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
fmt.Printf("Project '%s' created successfully\n", ProjectName)
|
||||
return nil
|
||||
}}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.Public, "public", "", false, "Project is public or private")
|
||||
flags.StringVarP(&opts.RegistryID, "registry-id", "", "", "ID of referenced registry when creating the proxy cache project")
|
||||
flags.StringVarP(&opts.StorageLimit, "storage-limit", "", "-1", "Storage quota of the project")
|
||||
flags.StringVarP(&opts.StorageLimit, "storage-limit", "", "", "Storage quota of the project")
|
||||
flags.BoolVarP(&opts.ProxyCache, "proxy-cache", "", false, "Whether the project is a proxy cache project")
|
||||
|
||||
return cmd
|
||||
|
@ -57,8 +91,9 @@ func createProjectView(createView *create.CreateView) error {
|
|||
}
|
||||
}
|
||||
|
||||
create.CreateProjectView(createView)
|
||||
|
||||
err := create.CreateProjectView(createView)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return api.CreateProject(*createView)
|
||||
|
||||
}
|
||||
|
|
|
@ -1,8 +1,26 @@
|
|||
// 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 (
|
||||
"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"
|
||||
)
|
||||
|
@ -10,28 +28,107 @@ import (
|
|||
// DeleteProjectCommand creates a new `harbor delete project` command
|
||||
func DeleteProjectCommand() *cobra.Command {
|
||||
var forceDelete bool
|
||||
var projectID string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete",
|
||||
Short: "delete project by name or id",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
Use: "delete",
|
||||
Short: "Delete project by name or ID",
|
||||
Example: "harbor project delete [projectname1] [projectname2] or harbor project delete --project-id [projectid]",
|
||||
Long: "Delete project by name or ID. Multiple projects can be deleted by providing their names as arguments. If no arguments are provided, it will prompt for the project name. Use --project-id to specify the project ID for single project directly. The --force flag will delete all repositories and artifacts within the project.",
|
||||
Args: cobra.MinimumNArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
|
||||
if len(args) > 0 {
|
||||
err = api.DeleteProject(args[0], forceDelete)
|
||||
successfulDeletes := []string{}
|
||||
failedDeletes := map[string]string{}
|
||||
|
||||
if projectID != "" {
|
||||
if len(args) > 0 {
|
||||
return fmt.Errorf("--project-id cannot be used with additional arguments")
|
||||
}
|
||||
if _, err := strconv.Atoi(projectID); err != nil {
|
||||
return fmt.Errorf("--project-id must be a numeric value")
|
||||
}
|
||||
}
|
||||
|
||||
if projectID != "" {
|
||||
log.Debugf("Deleting project with ID: %s", projectID)
|
||||
wg.Add(1)
|
||||
go func(id string) {
|
||||
defer wg.Done()
|
||||
if err := api.DeleteProject(id, forceDelete, true); err != nil {
|
||||
mu.Lock()
|
||||
failedDeletes[id] = utils.ParseHarborErrorMsg(err)
|
||||
mu.Unlock()
|
||||
} else {
|
||||
mu.Lock()
|
||||
successfulDeletes = append(successfulDeletes, id)
|
||||
mu.Unlock()
|
||||
}
|
||||
}(projectID)
|
||||
} else if len(args) > 0 {
|
||||
// Delete by project name from args
|
||||
log.Debugf("Deleting %d projects from args...", len(args))
|
||||
for _, projectName := range args {
|
||||
pn := projectName
|
||||
log.Debugf("Initiating delete for project: %s", pn)
|
||||
wg.Add(1)
|
||||
go func(projectName string) {
|
||||
defer wg.Done()
|
||||
log.Debugf("Deleting project '%s' with force=%v", projectName, forceDelete)
|
||||
if err := api.DeleteProject(projectName, forceDelete, false); err != nil {
|
||||
mu.Lock()
|
||||
failedDeletes[projectName] = utils.ParseHarborErrorMsg(err)
|
||||
mu.Unlock()
|
||||
} else {
|
||||
mu.Lock()
|
||||
successfulDeletes = append(successfulDeletes, projectName)
|
||||
mu.Unlock()
|
||||
}
|
||||
}(pn)
|
||||
}
|
||||
} else {
|
||||
projectName := prompt.GetProjectNameFromUser()
|
||||
err = api.DeleteProject(projectName, forceDelete)
|
||||
// If no arguments provided, prompt user for project name
|
||||
log.Debug("No arguments provided. Prompting user for project name.")
|
||||
projectName, err := prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
log.Debugf("User input project: %s", projectName)
|
||||
log.Debugf("Deleting project '%s' with force=%v", projectName, forceDelete)
|
||||
if err := api.DeleteProject(projectName, forceDelete, false); err != nil {
|
||||
return fmt.Errorf("failed to delete project: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
fmt.Printf("Project '%s' deleted successfully\n", projectName)
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("failed to delete project: %v", err)
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if len(successfulDeletes) > 0 {
|
||||
fmt.Println("Successfully deleted projects:")
|
||||
for _, name := range successfulDeletes {
|
||||
fmt.Printf(" - %s\n", name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(failedDeletes) > 0 {
|
||||
fmt.Println("Failed to delete projects:")
|
||||
for name, reason := range failedDeletes {
|
||||
fmt.Printf(" - %s: %s\n", name, reason)
|
||||
}
|
||||
return fmt.Errorf("failed to delete %d project(s)", len(failedDeletes))
|
||||
}
|
||||
|
||||
log.Debug("All requested projects deleted successfully.")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVar(&forceDelete, "force", false, "Deletes all repositories and artifacts within the project")
|
||||
flags.BoolVar(&forceDelete, "force", false, "Forcefully delete all repositories, artifacts, and policies in the project. Use with extreme caution—this action is irreversible.")
|
||||
flags.StringVar(&projectID, "project-id", "", "Specify project ID instead of project name")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
@ -1,7 +1,23 @@
|
|||
// 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 (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/go-client/pkg/sdk/v2.0/client/project"
|
||||
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
list "github.com/goharbor/harbor-cli/pkg/views/project/list"
|
||||
|
@ -14,43 +30,67 @@ func ListProjectCommand() *cobra.Command {
|
|||
var opts api.ListFlags
|
||||
var private bool
|
||||
var public bool
|
||||
var projects project.ListProjectsOK
|
||||
var allProjects []*models.Project
|
||||
var err error
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "list project",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if private && public {
|
||||
log.Fatal("Cannot specify both --private and --public flags")
|
||||
} else if private {
|
||||
opts.Public = false
|
||||
projects, err = api.ListProject(opts)
|
||||
} else if public {
|
||||
opts.Public = true
|
||||
projects, err = api.ListProject(opts)
|
||||
} else {
|
||||
projects, err = api.ListAllProjects(opts)
|
||||
Short: "List projects",
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
log.Debug("Starting project list command")
|
||||
|
||||
if opts.PageSize > 100 {
|
||||
return fmt.Errorf("page size should be less than or equal to 100")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get projects list: %v", err)
|
||||
if private && public {
|
||||
return fmt.Errorf("Cannot specify both --private and --public flags")
|
||||
}
|
||||
FormatFlag := viper.GetString("output-format")
|
||||
if FormatFlag != "" {
|
||||
err = utils.PrintFormat(projects, FormatFlag)
|
||||
|
||||
var listFunc func(...api.ListFlags) (project.ListProjectsOK, error)
|
||||
if private {
|
||||
log.Debug("Using private project list function")
|
||||
opts.Public = false
|
||||
listFunc = api.ListProject
|
||||
} else if public {
|
||||
log.Debug("Using public project list function")
|
||||
opts.Public = true
|
||||
listFunc = api.ListProject
|
||||
} else {
|
||||
log.Debug("Using list all projects function")
|
||||
listFunc = api.ListAllProjects
|
||||
}
|
||||
|
||||
log.Debug("Fetching projects...")
|
||||
allProjects, err = fetchProjects(listFunc, opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get projects list: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
|
||||
log.WithField("count", len(allProjects)).Debug("Number of projects fetched")
|
||||
if len(allProjects) == 0 {
|
||||
log.Info("No projects found")
|
||||
return nil
|
||||
}
|
||||
formatFlag := viper.GetString("output-format")
|
||||
if formatFlag != "" {
|
||||
log.WithField("output_format", formatFlag).Debug("Output format selected")
|
||||
err = utils.PrintFormat(allProjects, formatFlag)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
list.ListProjects(projects.Payload)
|
||||
log.Debug("Listing projects using default view")
|
||||
list.ListProjects(allProjects)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opts.Name, "name", "", "", "Name of the project")
|
||||
flags.Int64VarP(&opts.Page, "page", "", 1, "Page number")
|
||||
flags.Int64VarP(&opts.PageSize, "page-size", "", 10, "Size of per page")
|
||||
flags.Int64VarP(&opts.PageSize, "page-size", "", 0, "Size of per page (0 to fetch all)")
|
||||
flags.BoolVarP(&private, "private", "", false, "Show only private projects")
|
||||
flags.BoolVarP(&public, "public", "", false, "Show only public projects")
|
||||
flags.StringVarP(&opts.Q, "query", "q", "", "Query string to query resources")
|
||||
|
@ -58,3 +98,47 @@ func ListProjectCommand() *cobra.Command {
|
|||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func fetchProjects(listFunc func(...api.ListFlags) (project.ListProjectsOK, error), opts api.ListFlags) ([]*models.Project, error) {
|
||||
var allProjects []*models.Project
|
||||
if opts.PageSize == 0 {
|
||||
log.Debug("Page size is 0, will fetch all pages")
|
||||
opts.PageSize = 100
|
||||
opts.Page = 1
|
||||
|
||||
for {
|
||||
log.WithFields(log.Fields{
|
||||
"page": opts.Page,
|
||||
"page_size": opts.PageSize,
|
||||
}).Debug("Fetching next page of projects")
|
||||
|
||||
projects, err := listFunc(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.WithField("fetched_count", len(projects.Payload)).Debug("Fetched projects from current page")
|
||||
allProjects = append(allProjects, projects.Payload...)
|
||||
|
||||
if len(projects.Payload) < int(opts.PageSize) {
|
||||
log.Debug("Last page reached, stopping pagination")
|
||||
break
|
||||
}
|
||||
|
||||
opts.Page++
|
||||
}
|
||||
} else {
|
||||
log.WithFields(log.Fields{
|
||||
"page": opts.Page,
|
||||
"page_size": opts.PageSize,
|
||||
}).Debug("Fetching projects with user-defined pagination")
|
||||
|
||||
projects, err := listFunc(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allProjects = projects.Payload
|
||||
}
|
||||
|
||||
return allProjects, nil
|
||||
}
|
||||
|
|
|
@ -1,7 +1,22 @@
|
|||
// 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/go-client/pkg/sdk/v2.0/client/project"
|
||||
"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"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
|
@ -13,36 +28,63 @@ import (
|
|||
)
|
||||
|
||||
func LogsProjectCommmand() *cobra.Command {
|
||||
var opts api.ListFlags
|
||||
|
||||
cmd := &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 *project.GetLogsOK
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
}
|
||||
|
||||
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,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
|
||||
}
|
|
@ -1,6 +1,21 @@
|
|||
// 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 (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/project/list"
|
||||
|
@ -14,19 +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))
|
||||
}
|
||||
FormatFlag := viper.GetString("output-format")
|
||||
if FormatFlag != "" {
|
||||
utils.PrintPayloadInJSONFormat(projects)
|
||||
return
|
||||
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])
|
||||
}
|
||||
|
||||
list.SearchProjects(projects.Payload.Project)
|
||||
FormatFlag := viper.GetString("output-format")
|
||||
if FormatFlag != "" {
|
||||
err = utils.PrintFormat(projects, FormatFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
list.SearchProjects(projects.Payload.Project)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
|
|
|
@ -1,6 +1,21 @@
|
|||
// 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 (
|
||||
"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"
|
||||
|
@ -11,44 +26,56 @@ import (
|
|||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// GetProjectCommand creates a new `harbor get project` command
|
||||
func ViewCommand() *cobra.Command {
|
||||
|
||||
var isID bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "view [NAME|ID]",
|
||||
Short: "get project by name or id",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
var projectName string
|
||||
var project *project.GetProjectOK
|
||||
|
||||
if len(args) > 0 {
|
||||
log.Debugf("Project name provided: %s", args[0])
|
||||
projectName = args[0]
|
||||
} else {
|
||||
projectName = prompt.GetProjectNameFromUser()
|
||||
log.Debug("No project name provided, prompting user")
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
}
|
||||
|
||||
project, err = api.GetProject(projectName)
|
||||
|
||||
log.Debugf("Checking existence of project: %s", projectName)
|
||||
projectExists, err := api.CheckProject(projectName)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get project: %v", err)
|
||||
return
|
||||
return fmt.Errorf("failed to find project: %v ", utils.ParseHarborErrorMsg(err))
|
||||
} else if !projectExists {
|
||||
return fmt.Errorf("project %s does not exist", projectName)
|
||||
}
|
||||
log.Debugf("Project %s exists", projectName)
|
||||
project, err = api.GetProject(projectName, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get project: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
|
||||
FormatFlag := viper.GetString("output-format")
|
||||
if FormatFlag != "" {
|
||||
err = utils.PrintFormat(project, FormatFlag)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
view.ViewProjects(project.Payload)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVar(&isID, "id", false, "Get project by id")
|
||||
|
||||
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 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
|
||||
}
|
|
@ -1,3 +1,16 @@
|
|||
// 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 registry
|
||||
|
||||
import (
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
// 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 registry
|
||||
|
||||
import (
|
||||
|
@ -14,7 +27,7 @@ func CreateRegistryCommand() *cobra.Command {
|
|||
Use: "create",
|
||||
Short: "create registry",
|
||||
Example: "harbor registry create",
|
||||
Args: cobra.NoArgs,
|
||||
Args: cobra.ExactArgs(0),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
createView := &api.CreateRegView{
|
||||
|
|
|
@ -1,6 +1,21 @@
|
|||
// 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 registry
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
@ -8,24 +23,51 @@ import (
|
|||
)
|
||||
|
||||
func DeleteRegistryCommand() *cobra.Command {
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete",
|
||||
Short: "delete registry by id",
|
||||
Short: "delete registry by name or id",
|
||||
Example: "harbor registry delete [registryname]",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Args: cobra.MinimumNArgs(0),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errChan := make(chan error, len(args))
|
||||
if len(args) > 0 {
|
||||
registryName, _ := api.GetRegistryIdByName(args[0])
|
||||
err = api.DeleteRegistry(registryName)
|
||||
for _, arg := range args {
|
||||
registryID, _ := api.GetRegistryIdByName(arg)
|
||||
wg.Add(1)
|
||||
go func(registryID int64) {
|
||||
defer wg.Done()
|
||||
if err := api.DeleteRegistry(registryID); err != nil {
|
||||
errChan <- err
|
||||
}
|
||||
}(registryID)
|
||||
}
|
||||
} else {
|
||||
registryId := prompt.GetRegistryNameFromUser()
|
||||
err = api.DeleteRegistry(registryId)
|
||||
err := api.DeleteRegistry(registryId)
|
||||
if err != nil {
|
||||
log.Errorf("failed to delete registry: %v", err)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("failed to delete registry: %v", err)
|
||||
|
||||
// Wait for all goroutines to finish
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(errChan)
|
||||
}()
|
||||
|
||||
// Collect and handle errors
|
||||
var finalErr error
|
||||
for err := range errChan {
|
||||
if finalErr == nil {
|
||||
finalErr = err
|
||||
} else {
|
||||
log.Errorf("Error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if finalErr != nil {
|
||||
log.Errorf("failed to delete registry: %v", finalErr)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,6 +1,21 @@
|
|||
// 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 registry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/registry/list"
|
||||
|
@ -16,21 +31,30 @@ func ListRegistryCommand() *cobra.Command {
|
|||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "list registry",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if opts.PageSize > 100 {
|
||||
return fmt.Errorf("page size should be less than or equal to 100")
|
||||
}
|
||||
registry, err := api.ListRegistries(opts)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get projects list: %v", err)
|
||||
return fmt.Errorf("failed to get projects list: %v", err)
|
||||
}
|
||||
FormatFlag := viper.GetString("output-format")
|
||||
if FormatFlag != "" {
|
||||
err = utils.PrintFormat(registry, FormatFlag)
|
||||
if len(registry.Payload) == 0 {
|
||||
log.Info("No registries found")
|
||||
return nil
|
||||
}
|
||||
formatFlag := viper.GetString("output-format")
|
||||
if formatFlag != "" {
|
||||
err = utils.PrintFormat(registry, formatFlag)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
} else {
|
||||
list.ListRegistry(registry.Payload)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -1,103 +1,117 @@
|
|||
// 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 registry
|
||||
|
||||
import (
|
||||
"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/registry/create"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/registry/update"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewUpdateRegistryCommand creates a new `harbor update registry` command
|
||||
func UpdateRegistryCommand() *cobra.Command {
|
||||
var opts api.CreateRegView
|
||||
opts := &models.Registry{
|
||||
Credential: &models.RegistryCredential{},
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "update",
|
||||
Short: "update registry",
|
||||
Example: "harbor registry update [registryname]",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Use: "update [registry_name]",
|
||||
Short: "update registry",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
var registryId int64
|
||||
|
||||
updateView := &api.CreateRegView{
|
||||
Name: opts.Name,
|
||||
Type: opts.Type,
|
||||
Description: opts.Description,
|
||||
URL: opts.URL,
|
||||
Credential: api.RegistryCredential{
|
||||
AccessKey: opts.Credential.AccessKey,
|
||||
Type: opts.Credential.Type,
|
||||
AccessSecret: opts.Credential.AccessSecret,
|
||||
},
|
||||
Insecure: opts.Insecure,
|
||||
}
|
||||
|
||||
if len(args) > 0 {
|
||||
registryId, err = api.GetRegistryIdByName(args[0])
|
||||
if err != nil {
|
||||
log.Errorf("failed to get registry id: %v", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
registryId = prompt.GetRegistryNameFromUser()
|
||||
}
|
||||
|
||||
existingRegistry, err := api.GetRegistryResponse(registryId)
|
||||
if err != nil {
|
||||
log.Errorf("failed to parse registry id: %v", err)
|
||||
log.Errorf("failed to get registry with ID %d: %v", registryId, err)
|
||||
return
|
||||
}
|
||||
if existingRegistry == nil {
|
||||
log.Errorf("registry is not found")
|
||||
return
|
||||
}
|
||||
|
||||
if opts.Name != "" && opts.Type != "" && opts.URL != "" {
|
||||
err = api.UpdateRegistry(updateView, registryId)
|
||||
} else {
|
||||
err = updateRegistryView(updateView, registryId)
|
||||
updateView := &models.Registry{
|
||||
Name: existingRegistry.Name,
|
||||
Type: existingRegistry.Type,
|
||||
Description: existingRegistry.Description,
|
||||
URL: existingRegistry.URL,
|
||||
Insecure: existingRegistry.Insecure,
|
||||
Credential: &models.RegistryCredential{
|
||||
AccessKey: existingRegistry.Credential.AccessKey,
|
||||
AccessSecret: existingRegistry.Credential.AccessSecret,
|
||||
Type: existingRegistry.Credential.Type,
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
if flags.Changed("name") {
|
||||
updateView.Name = opts.Name
|
||||
}
|
||||
if flags.Changed("type") {
|
||||
updateView.Type = opts.Type
|
||||
}
|
||||
if flags.Changed("description") {
|
||||
updateView.Description = opts.Description
|
||||
}
|
||||
if flags.Changed("url") {
|
||||
updateView.URL = opts.URL
|
||||
}
|
||||
if flags.Changed("insecure") {
|
||||
updateView.Insecure = opts.Insecure
|
||||
}
|
||||
if flags.Changed("credential-access-key") {
|
||||
updateView.Credential.AccessKey = opts.Credential.AccessKey
|
||||
}
|
||||
if flags.Changed("credential-access-secret") {
|
||||
updateView.Credential.AccessSecret = opts.Credential.AccessSecret
|
||||
}
|
||||
if flags.Changed("credential-type") {
|
||||
updateView.Credential.Type = opts.Credential.Type
|
||||
}
|
||||
|
||||
update.UpdateRegistryView(updateView)
|
||||
err = api.UpdateRegistry(updateView, registryId)
|
||||
if err != nil {
|
||||
log.Errorf("failed to update registry: %v", err)
|
||||
return
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opts.Name, "name", "", "", "Name of the registry")
|
||||
flags.StringVarP(&opts.Type, "type", "", "", "Type of the registry")
|
||||
flags.StringVarP(&opts.URL, "url", "", "", "Registry endpoint URL")
|
||||
flags.StringVarP(&opts.Description, "description", "", "", "Description of the registry")
|
||||
flags.BoolVarP(
|
||||
&opts.Insecure,
|
||||
"insecure",
|
||||
"",
|
||||
true,
|
||||
"Whether or not the certificate will be verified when Harbor tries to access the server",
|
||||
)
|
||||
flags.StringVarP(
|
||||
&opts.Credential.AccessKey,
|
||||
"credential-access-key",
|
||||
"",
|
||||
"",
|
||||
"Access key, e.g. user name when credential type is 'basic'",
|
||||
)
|
||||
flags.StringVarP(
|
||||
&opts.Credential.AccessSecret,
|
||||
"credential-access-secret",
|
||||
"",
|
||||
"",
|
||||
"Access secret, e.g. password when credential type is 'basic'",
|
||||
)
|
||||
flags.StringVarP(
|
||||
&opts.Credential.Type,
|
||||
"credential-type",
|
||||
"",
|
||||
"",
|
||||
"Credential type, such as 'basic', 'oauth'",
|
||||
)
|
||||
flags.StringVarP(&opts.Name, "name", "n", "", "Name of the registry")
|
||||
flags.StringVarP(&opts.Type, "type", "t", "", "Type of the registry")
|
||||
flags.StringVarP(&opts.URL, "url", "u", "", "Registry endpoint URL")
|
||||
flags.StringVarP(&opts.Description, "description", "d", "", "Description of the registry")
|
||||
flags.BoolVarP(&opts.Insecure, "insecure", "i", false, "Whether or not the certificate will be verified when Harbor tries to access the server")
|
||||
flags.StringVarP(&opts.Credential.AccessKey, "credential-access-key", "k", "", "Access key, e.g. user name when credential type is 'basic'")
|
||||
flags.StringVarP(&opts.Credential.AccessSecret, "credential-access-secret", "s", "", "Access secret, e.g. password when credential type is 'basic'")
|
||||
flags.StringVarP(&opts.Credential.Type, "credential-type", "", "", "Credential type, such as 'basic', 'oauth'")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func updateRegistryView(updateView *api.CreateRegView, projectID int64) error {
|
||||
if updateView == nil {
|
||||
updateView = &api.CreateRegView{}
|
||||
}
|
||||
|
||||
create.CreateRegistryView(updateView)
|
||||
return api.UpdateRegistry(updateView, projectID)
|
||||
}
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
// 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 registry
|
||||
|
||||
import (
|
||||
|
@ -33,7 +46,6 @@ func ViewRegistryCommand() *cobra.Command {
|
|||
}
|
||||
|
||||
registry, err = api.ViewRegistry(registryId)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("failed to get registry info: %v", err)
|
||||
return
|
||||
|
@ -48,7 +60,6 @@ func ViewRegistryCommand() *cobra.Command {
|
|||
} else {
|
||||
view.ViewRegistry(registry.Payload)
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
// 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 (
|
||||
|
@ -9,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
|
||||
}
|
|
@ -1,3 +1,16 @@
|
|||
// 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 repository
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
@ -16,5 +29,4 @@ func Repository() *cobra.Command {
|
|||
)
|
||||
|
||||
return cmd
|
||||
|
||||
}
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
// 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 repository
|
||||
|
||||
import (
|
||||
|
@ -16,14 +29,22 @@ func RepoDeleteCmd() *cobra.Command {
|
|||
Long: `Delete a repository within a project in Harbor`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
var projectName string
|
||||
var repoName string
|
||||
if len(args) > 0 {
|
||||
projectName, repoName := utils.ParseProjectRepo(args[0])
|
||||
err = api.RepoDelete(projectName, repoName)
|
||||
projectName, repoName, err = utils.ParseProjectRepo(args[0])
|
||||
if err != nil {
|
||||
log.Errorf("failed to parse project/repo: %v", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
projectName := prompt.GetProjectNameFromUser()
|
||||
repoName := prompt.GetRepoNameFromUser(projectName)
|
||||
err = api.RepoDelete(projectName, repoName)
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
repoName = prompt.GetRepoNameFromUser(projectName)
|
||||
}
|
||||
err = api.RepoDelete(projectName, repoName, false)
|
||||
if err != nil {
|
||||
log.Errorf("failed to delete repository: %v", err)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,21 @@
|
|||
// 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 repository
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/go-client/pkg/sdk/v2.0/client/repository"
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
|
@ -12,13 +27,18 @@ import (
|
|||
)
|
||||
|
||||
func ListRepositoryCommand() *cobra.Command {
|
||||
var opts api.ListFlags
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "list repositories within a project",
|
||||
Example: ` harbor repo list <project_name>`,
|
||||
Long: `Get information of all repositories in a project`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if opts.PageSize > 100 {
|
||||
return fmt.Errorf("page size should be less than or equal to 100")
|
||||
}
|
||||
var err error
|
||||
var repos repository.ListRepositoriesOK
|
||||
var projectName string
|
||||
|
@ -26,15 +46,20 @@ func ListRepositoryCommand() *cobra.Command {
|
|||
if len(args) > 0 {
|
||||
projectName = args[0]
|
||||
} else {
|
||||
projectName = prompt.GetProjectNameFromUser()
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
}
|
||||
|
||||
repos, err = api.ListRepository(projectName)
|
||||
|
||||
repos, err = api.ListRepository(projectName, false, opts)
|
||||
if err != nil {
|
||||
log.Errorf("failed to list repositories: %v", err)
|
||||
return fmt.Errorf("failed to list repositories: %v", err)
|
||||
}
|
||||
if len(repos.Payload) == 0 {
|
||||
log.Info("No repositories found")
|
||||
return nil
|
||||
}
|
||||
|
||||
FormatFlag := viper.GetString("output-format")
|
||||
if FormatFlag != "" {
|
||||
err = utils.PrintFormat(repos, FormatFlag)
|
||||
|
@ -44,8 +69,15 @@ func ListRepositoryCommand() *cobra.Command {
|
|||
} else {
|
||||
list.ListRepositories(repos.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.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
|
||||
}
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
// 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 repository
|
||||
|
||||
import (
|
||||
|
@ -15,18 +28,21 @@ func SearchRepoCmd() *cobra.Command {
|
|||
Short: "search repository based on their names",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
repo, err := api.SearchRepository(args[0])
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get repositories: %v", err)
|
||||
}
|
||||
FormatFlag := viper.GetString("output-format")
|
||||
if FormatFlag != "" {
|
||||
utils.PrintPayloadInJSONFormat(repo)
|
||||
return
|
||||
}
|
||||
|
||||
search.SearchRepositories(repo.Payload.Repository)
|
||||
FormatFlag := viper.GetString("output-format")
|
||||
if FormatFlag != "" {
|
||||
err = utils.PrintFormat(repo, FormatFlag)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
} else {
|
||||
search.SearchRepositories(repo.Payload.Repository)
|
||||
}
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
|
|
|
@ -1 +1,14 @@
|
|||
// 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 repository
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
// 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 repository
|
||||
|
||||
import (
|
||||
|
@ -23,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
|
||||
|
@ -46,7 +65,6 @@ func RepoViewCmd() *cobra.Command {
|
|||
} else {
|
||||
view.ViewRepository(repo.Payload)
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue