Compare commits
46 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 |
|
@ -2,3 +2,4 @@
|
|||
/internal/dagger
|
||||
/internal/querybuilder
|
||||
/internal/telemetry
|
||||
/.env
|
||||
|
|
|
@ -38,6 +38,20 @@ Runs `golangci-lint` on your code and saves the report to a file.
|
|||
dagger call lint-report export --path=./LintReport.json
|
||||
```
|
||||
|
||||
### 📝 `TestCoverageReport()`
|
||||
|
||||
Runs go test coverage tools and creates a report.
|
||||
```bash
|
||||
dagger call test-coverage-report export --path=coverage-report.md
|
||||
```
|
||||
|
||||
### ✅ `CheckCoverageThreshold(context, threshold)`
|
||||
|
||||
Runs go test coverage tools and creates a report. The total coverage is compared to a threshold that can be set to e.g. 80%.
|
||||
```bash
|
||||
dagger call check-coverage-threshold --threshold 80.0
|
||||
```
|
||||
|
||||
### 🚀 `PublishImage(registry, imageTags)`
|
||||
|
||||
Builds and publishes the Harbor CLI image to the given container registry with proper OCI metadata labels.
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
module dagger/harbor-cli
|
||||
|
||||
go 1.23.1
|
||||
go 1.24.4
|
||||
|
||||
require (
|
||||
github.com/99designs/gqlgen v0.17.70
|
||||
github.com/Khan/genqlient v0.8.0
|
||||
github.com/vektah/gqlparser/v2 v2.5.23
|
||||
go.opentelemetry.io/otel v1.34.0
|
||||
github.com/99designs/gqlgen v0.17.74
|
||||
github.com/Khan/genqlient v0.8.1
|
||||
github.com/vektah/gqlparser/v2 v2.5.27
|
||||
go.opentelemetry.io/otel v1.35.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0
|
||||
go.opentelemetry.io/otel/log v0.8.0
|
||||
go.opentelemetry.io/otel/sdk v1.34.0
|
||||
go.opentelemetry.io/otel/sdk v1.35.0
|
||||
go.opentelemetry.io/otel/sdk/log v0.8.0
|
||||
go.opentelemetry.io/otel/trace v1.34.0
|
||||
go.opentelemetry.io/otel/trace v1.35.0
|
||||
go.opentelemetry.io/proto/otlp v1.3.1
|
||||
golang.org/x/sync v0.12.0
|
||||
google.golang.org/grpc v1.71.0
|
||||
golang.org/x/sync v0.15.0
|
||||
google.golang.org/grpc v1.73.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
@ -31,13 +31,13 @@ require (
|
|||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.34.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
)
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
github.com/99designs/gqlgen v0.17.70 h1:xgLIgQuG+Q2L/AE9cW595CT7xCWCe/bpPIFGSfsGSGs=
|
||||
github.com/99designs/gqlgen v0.17.70/go.mod h1:fvCiqQAu2VLhKXez2xFvLmE47QgAPf/KTPN5XQ4rsHQ=
|
||||
github.com/Khan/genqlient v0.8.0 h1:Hd1a+E1CQHYbMEKakIkvBH3zW0PWEeiX6Hp1i2kP2WE=
|
||||
github.com/Khan/genqlient v0.8.0/go.mod h1:hn70SpYjWteRGvxTwo0kfaqg4wxvndECGkfa1fdDdYI=
|
||||
github.com/99designs/gqlgen v0.17.74 h1:1FuVtkXxOc87xpKio3f6sohREmec+Jvy86PcYOuwgWo=
|
||||
github.com/99designs/gqlgen v0.17.74/go.mod h1:a+iR6mfRLNRp++kDpooFHiPWYiWX3Yu1BIilQRHgh10=
|
||||
github.com/Khan/genqlient v0.8.1 h1:wtOCc8N9rNynRLXN3k3CnfzheCUNKBcvXmVv5zt6WCs=
|
||||
github.com/Khan/genqlient v0.8.1/go.mod h1:R2G6DzjBvCbhjsEajfRjbWdVglSH/73kSivC9TLWVjU=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
|
@ -15,8 +15,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
|||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN9coASF1GusYX6AlU=
|
||||
|
@ -29,12 +29,12 @@ github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq
|
|||
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/vektah/gqlparser/v2 v2.5.23 h1:PurJ9wpgEVB7tty1seRUwkIDa/QH5RzkzraiKIjKLfA=
|
||||
github.com/vektah/gqlparser/v2 v2.5.23/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
|
||||
github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=
|
||||
github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8=
|
||||
|
@ -51,34 +51,34 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9
|
|||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI=
|
||||
go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk=
|
||||
go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8=
|
||||
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
|
||||
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs=
|
||||
go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
||||
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
||||
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
|
||||
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
|
||||
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
|
||||
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
|
|
@ -24,8 +24,7 @@ import (
|
|||
|
||||
const (
|
||||
GOLANGCILINT_VERSION = "v2.1.2"
|
||||
GO_VERSION = "1.24.2"
|
||||
SYFT_VERSION = "v1.9.0"
|
||||
GO_VERSION = "1.24.4"
|
||||
GORELEASER_VERSION = "v2.8.2"
|
||||
)
|
||||
|
||||
|
@ -237,32 +236,28 @@ func (m *HarborCli) Release(ctx context.Context, githubToken *dagger.Secret) {
|
|||
goreleaser := m.goreleaserContainer().
|
||||
WithSecretVariable("GITHUB_TOKEN", githubToken).
|
||||
WithExec([]string{"goreleaser", "release", "--clean"})
|
||||
_, err := goreleaser.Stderr(ctx)
|
||||
error, err := goreleaser.Stderr(ctx)
|
||||
if err != nil {
|
||||
log.Printf("Error occured during release: %s", err)
|
||||
return
|
||||
}
|
||||
if len(error) > 0 {
|
||||
log.Printf("Error occured while release: %s", err)
|
||||
return
|
||||
}
|
||||
log.Println("Release tasks completed successfully 🎉")
|
||||
}
|
||||
|
||||
// Return a container with the goreleaser binary mounted and the source directory mounted.
|
||||
func (m *HarborCli) goreleaserContainer() *dagger.Container {
|
||||
// Export the syft binary from the syft container as a file to generate SBOM
|
||||
syft := dag.Container().
|
||||
From(fmt.Sprintf("anchore/syft:%s", SYFT_VERSION)).
|
||||
WithMountedCache("/go/pkg/mod", dag.CacheVolume("syft-gomod")).
|
||||
File("/syft")
|
||||
|
||||
return dag.Container().
|
||||
From(fmt.Sprintf("goreleaser/goreleaser:%s", GORELEASER_VERSION)).
|
||||
WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+GO_VERSION)).
|
||||
WithEnvVariable("GOMODCACHE", "/go/pkg/mod").
|
||||
WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+GO_VERSION)).
|
||||
WithEnvVariable("GOCACHE", "/go/build-cache").
|
||||
WithFile("/bin/syft", syft).
|
||||
WithMountedDirectory("/src", m.Source).
|
||||
WithWorkdir("/src").
|
||||
WithEnvVariable("TINI_SUBREAPER", "true")
|
||||
WithWorkdir("/src")
|
||||
}
|
||||
|
||||
// Generate CLI Documentation and return the directory containing the generated files
|
||||
|
@ -295,6 +290,7 @@ func (m *HarborCli) Test(ctx context.Context) (string, error) {
|
|||
}
|
||||
|
||||
// Executes Go tests and returns TestReport in json file
|
||||
// TestReport executes Go tests and returns only the JSON report file
|
||||
func (m *HarborCli) TestReport(ctx context.Context) *dagger.File {
|
||||
reportName := "TestReport.json"
|
||||
test := dag.Container().
|
||||
|
@ -306,11 +302,64 @@ func (m *HarborCli) TestReport(ctx context.Context) *dagger.File {
|
|||
WithExec([]string{"go", "install", "gotest.tools/gotestsum@latest"}).
|
||||
WithMountedDirectory("/src", m.Source).
|
||||
WithWorkdir("/src").
|
||||
WithExec([]string{"gotestsum", "--jsonfile", reportName})
|
||||
WithExec([]string{"gotestsum", "--jsonfile", reportName, "./..."})
|
||||
|
||||
return test.File(reportName)
|
||||
}
|
||||
|
||||
func (m *HarborCli) TestCoverage(ctx context.Context) *dagger.File {
|
||||
coverage := "coverage.out"
|
||||
test := dag.Container().
|
||||
From("golang:"+GO_VERSION+"-alpine").
|
||||
WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+GO_VERSION)).
|
||||
WithEnvVariable("GOMODCACHE", "/go/pkg/mod").
|
||||
WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+GO_VERSION)).
|
||||
WithEnvVariable("GOCACHE", "/go/build-cache").
|
||||
WithExec([]string{"go", "install", "gotest.tools/gotestsum@latest"}).
|
||||
WithMountedDirectory("/src", m.Source).
|
||||
WithWorkdir("/src").
|
||||
WithExec([]string{"gotestsum", "--", "-coverprofile=" + coverage, "./..."})
|
||||
|
||||
return test.File(coverage)
|
||||
}
|
||||
|
||||
// TestCoverageReport processes coverage data and returns a formatted markdown report
|
||||
func (m *HarborCli) TestCoverageReport(ctx context.Context) *dagger.File {
|
||||
coverageFile := "coverage.out"
|
||||
reportFile := "coverage-report.md"
|
||||
test := dag.Container().
|
||||
From("golang:"+GO_VERSION+"-alpine").
|
||||
WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+GO_VERSION)).
|
||||
WithEnvVariable("GOMODCACHE", "/go/pkg/mod").
|
||||
WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+GO_VERSION)).
|
||||
WithEnvVariable("GOCACHE", "/go/build-cache").
|
||||
WithMountedDirectory("/src", m.Source).
|
||||
WithWorkdir("/src").
|
||||
WithExec([]string{"apk", "add", "--no-cache", "bc"}).
|
||||
WithExec([]string{"go", "test", "-coverprofile=" + coverageFile, "./..."})
|
||||
return test.WithExec([]string{"sh", "-c", `
|
||||
echo "<h2> 📊 Test Coverage Results</h2>" > ` + reportFile + `
|
||||
if [ ! -f "` + coverageFile + `" ]; then
|
||||
echo "<p>❌ Coverage file not found!</p>" >> ` + reportFile + `
|
||||
exit 1
|
||||
fi
|
||||
total_coverage=$(go tool cover -func=` + coverageFile + ` | grep total: | grep -Eo '[0-9]+\.[0-9]+')
|
||||
echo "DEBUG: Total coverage is $total_coverage" >&2
|
||||
if (( $(echo "$total_coverage >= 80.0" | bc -l) )); then
|
||||
emoji="✅"
|
||||
elif (( $(echo "$total_coverage >= 60.0" | bc -l) )); then
|
||||
emoji="⚠️"
|
||||
else
|
||||
emoji="❌"
|
||||
fi
|
||||
echo "<p><b>Total coverage: $emoji $total_coverage% (Target: 80%)</b></p>" >> ` + reportFile + `
|
||||
echo "<details><summary>Detailed package coverage</summary><pre>" >> ` + reportFile + `
|
||||
go tool cover -func=` + coverageFile + ` >> ` + reportFile + `
|
||||
echo "</pre></details>" >> ` + reportFile + `
|
||||
cat ` + reportFile + ` >&2
|
||||
`}).File(reportFile)
|
||||
}
|
||||
|
||||
// Checks for vulnerabilities using govulncheck
|
||||
func (m *HarborCli) vulnerabilityCheck(ctx context.Context) *dagger.Container {
|
||||
return dag.Container().
|
||||
|
|
|
@ -31,17 +31,17 @@ jobs:
|
|||
|
||||
- name: Check for changes
|
||||
run: |
|
||||
# Check if any newly added docs exist
|
||||
untracked_files=$(git ls-files --others --exclude-standard)
|
||||
# Check if any docs have been modified
|
||||
changed_files=$(git ls-files --others --modified --deleted --exclude-standard)
|
||||
|
||||
# If there are untracked files, fail the workflow
|
||||
if [ -n "$untracked_files" ]; then
|
||||
echo "New Untracked files found"
|
||||
echo "please check if docs were added for new commands"
|
||||
echo "$untracked_files"
|
||||
# If there are files changed, fail the workflow
|
||||
if [ -n "$changed_files" ]; then
|
||||
echo "file changes found"
|
||||
echo "please check if docs were added for new commands or updated for new commands"
|
||||
echo "$changed_files"
|
||||
exit 1 # This will fail the workflow
|
||||
else
|
||||
echo "No untracked files found."
|
||||
echo "No file changes found."
|
||||
fi
|
||||
continue-on-error: false
|
||||
|
||||
|
@ -137,6 +137,33 @@ jobs:
|
|||
with:
|
||||
fromJSONFile: TestReport.json
|
||||
|
||||
- name: Run Test Coverage Report
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: dagger/dagger-for-github@v7
|
||||
with:
|
||||
version: ${{ steps.dagger_version.outputs.version }}
|
||||
verb: call
|
||||
args: test-coverage-report export --path=coverage-report.md
|
||||
|
||||
- name: Add coverage to step summary
|
||||
if: github.event_name == 'pull_request'
|
||||
run: cat coverage-report.md >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Run Test Coverage
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: dagger/dagger-for-github@v7
|
||||
with:
|
||||
version: ${{ steps.dagger_version.outputs.version }}
|
||||
verb: call
|
||||
args: test-coverage export --path=coverage.out
|
||||
|
||||
- uses: codecov/codecov-action@v5
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
verbose: true
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- name: Build Binary
|
||||
uses: dagger/dagger-for-github@v7
|
||||
with:
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
version: 2
|
||||
project_name: harbor
|
||||
project_name: harbor-cli
|
||||
|
||||
before:
|
||||
hooks:
|
||||
|
@ -63,6 +63,22 @@ release:
|
|||
owner: goharbor # Your GitHub repository owner
|
||||
name: harbor-cli # Your GitHub repository name
|
||||
|
||||
# https://goreleaser.com/customization/homebrew/
|
||||
brews:
|
||||
- repository:
|
||||
owner: goharbor # GitHub user/org who owns the tap repo
|
||||
name: homebrew-tap # Tap repo name (i.e., goharbor/homebrew-tap)
|
||||
branch: main
|
||||
name: harbor-cli # Name of the CLI, becomes harbor-cli.rb
|
||||
commit_author: # Who commits to the tap repo
|
||||
name: goreleaserbot
|
||||
email: bot@goreleaser.com
|
||||
commit_msg_template: "Brew formula update for {{ .ProjectName }} version {{ .Tag }}"
|
||||
homepage: "https://goharbor.io"
|
||||
description: "Harbor CLI for interacting with Harbor registry" # Formula description
|
||||
test: |
|
||||
system "#{bin}/harbor-cli", "version" # Formula test (after install)
|
||||
|
||||
changelog:
|
||||
use: github
|
||||
filters:
|
||||
|
|
80
README.md
80
README.md
|
@ -1,28 +1,58 @@
|
|||

|
||||
|
||||
**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/.config/harbor-cli/config.yaml:/root/.config/harbor-cli/config.yaml \
|
||||
|
@ -32,7 +62,11 @@ docker run -ti --rm -v $HOME/.config/harbor-cli/config.yaml:/root/.config/harbor
|
|||
```
|
||||
Use the `HARBOR_ENCRYPTION_KEY` container environment variable as a base64-encoded 32-byte key for AES-256 encryption. This securely stores your harbor login password.
|
||||
|
||||
# Add the following command to create an alias and append the alias to your .zshrc or .bashrc file
|
||||
If you intend
|
||||
to run the CLI as a container,it is advised
|
||||
to set the following environment variables and to create an alias
|
||||
and append the alias to your .zshrc or .bashrc file
|
||||
|
||||
```shell
|
||||
echo "export HARBOR_CLI_CONFIG=\$HOME/.config/harbor-cli/config.yaml" >> ~/.zshrc
|
||||
echo "export HARBOR_ENCRYPTION_KEY=\$(cat <path_to_32bit_private_key_file> | base64)" >> ~/.zshrc
|
||||
|
@ -45,14 +79,14 @@ source ~/.zshrc # or restart your terminal
|
|||
|
||||
Harbor CLI will soon be published on Homebrew.
|
||||
Meantime, we recommend using Harbor in the Container
|
||||
or download the binary from the [releases page](https://github.com/goharbor/harbor-cli/releases)
|
||||
or downloading the binary from the [releases page](https://github.com/goharbor/harbor-cli/releases)
|
||||
|
||||
|
||||
|
||||
## Add the Harbor CLI to your Container Image
|
||||
|
||||
Using Curl or Wget isn't recommended
|
||||
for adding the Harbor CLI to your container.
|
||||
Using Curl or Wget isn't needed if you want to
|
||||
add the Harbor CLI to your container.
|
||||
Instead, we recommend copying the Harbor CLI from our official image
|
||||
by using the following Dockerfile:
|
||||
|
||||
|
@ -115,15 +149,19 @@ Use "harbor [command] --help" for more information about a command.
|
|||
#### Config Management
|
||||
|
||||
##### Hierarchy
|
||||
Use the `--config` flag to specify a custom configuration file path (highest priority).
|
||||
```bash
|
||||
Use the `--config` flag to specify a custom configuration file path (the highest priority).
|
||||
|
||||
```bash
|
||||
harbor --config /path/to/custom/config.yaml artifact list
|
||||
```
|
||||
|
||||
If `--config` is not provided, Harbor CLI checks the `HARBOR_CLI_CONFIG` environment variable for the config file path.
|
||||
|
||||
```bash
|
||||
export HARBOR_CLI_CONFIG=/path/to/custom/config.yaml
|
||||
harbor artifact list
|
||||
```
|
||||
|
||||
If neither is set, it defaults to `$XDG_CONFIG_HOME/harbor-cli/config.yaml` or `$HOME/.config/harbor-cli/config.yaml` if `XDG_CONFIG_HOME` is unset.
|
||||
```bash
|
||||
harbor artifact list
|
||||
|
@ -190,9 +228,10 @@ Windows | ✅
|
|||
|
||||
# Build From Source
|
||||
|
||||
Make sure you have latest [Dagger](https://docs.dagger.io/) installed in your system.
|
||||
Make sure you have the latest [Dagger](https://docs.dagger.io/) installed in your system.
|
||||
|
||||
#### Using Dagger
|
||||
|
||||
```bash
|
||||
git clone https://github.com/goharbor/harbor-cli.git && cd harbor-cli
|
||||
dagger call build-dev --platform darwin/arm64 export --path=./harbor-cli
|
||||
|
@ -208,8 +247,8 @@ go build -o harbor-cli cmd/harbor/main.go
|
|||
|
||||
# Version Compatibility With Harbor
|
||||
|
||||
At the moment, the Harbor CLI is developed and tested with Harbor 2.11.
|
||||
The CLI should work with versions prior to 2.11,
|
||||
At the moment, the Harbor CLI is developed and tested with Harbor 2.13.
|
||||
The CLI should work with versions prior to 2.13,
|
||||
but not all functionalities may be available or work as expected.
|
||||
|
||||
Harbor <2.0.0 is not supported.
|
||||
|
@ -238,3 +277,4 @@ This project is maintained by the Harbor community. We thank all our contributor
|
|||
|
||||
For any questions or issues, please open an issue on our [GitHub Issues](https://github.com/goharbor/harbor-cli/issues) page.<br>
|
||||
Give a ⭐ if this project helped you, Thank YOU!
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
package artifact
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/artifact/label"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
@ -31,6 +32,7 @@ func Artifact() *cobra.Command {
|
|||
DeleteArtifactCommand(),
|
||||
ScanArtifactCommand(),
|
||||
ArtifactTagsCmd(),
|
||||
label.LabelsArtifactCommmand(),
|
||||
)
|
||||
|
||||
return cmd
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
package artifact
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
|
@ -26,11 +28,14 @@ func DeleteArtifactCommand() *cobra.Command {
|
|||
Use: "delete",
|
||||
Short: "delete an artifact",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
var projectName, repoName, reference string
|
||||
if len(args) > 0 {
|
||||
projectName, repoName, reference = utils.ParseProjectRepoReference(args[0])
|
||||
projectName, repoName, reference, err = utils.ParseProjectRepoReference(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse project/repo/reference: %v", err)
|
||||
}
|
||||
} else {
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
|
@ -41,8 +46,9 @@ func DeleteArtifactCommand() *cobra.Command {
|
|||
}
|
||||
err = api.DeleteArtifact(projectName, repoName, reference)
|
||||
if err != nil {
|
||||
log.Errorf("failed to delete an artifact: %v", err)
|
||||
return fmt.Errorf("failed to delete an artifact: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -51,7 +51,10 @@ Supports pagination, search queries, and sorting using flags.`,
|
|||
var projectName, repoName string
|
||||
|
||||
if len(args) > 0 {
|
||||
projectName, repoName = utils.ParseProjectRepo(args[0])
|
||||
projectName, repoName, err = utils.ParseProjectRepo(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse project/repo: %v", err)
|
||||
}
|
||||
} else {
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
package artifact
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
|
@ -44,24 +46,28 @@ func StartScanArtifactCommand() *cobra.Command {
|
|||
Short: "Start a scan of an artifact",
|
||||
Long: `Start a scan of an artifact in Harbor Repository`,
|
||||
Example: `harbor artifact scan start <project>/<repository>/<reference>`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
var projectName, repoName, reference string
|
||||
|
||||
if len(args) > 0 {
|
||||
projectName, repoName, reference = utils.ParseProjectRepoReference(args[0])
|
||||
projectName, repoName, reference, err = utils.ParseProjectRepoReference(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse project/repo/reference: %v", err)
|
||||
}
|
||||
} else {
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
|
||||
return fmt.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
repoName = prompt.GetRepoNameFromUser(projectName)
|
||||
reference = prompt.GetReferenceFromUser(repoName, projectName)
|
||||
}
|
||||
err = api.StartScanArtifact(projectName, repoName, reference)
|
||||
if err != nil {
|
||||
log.Errorf("failed to start scan of artifact: %v", err)
|
||||
return fmt.Errorf("failed to start scan of artifact: %v", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
|
@ -78,7 +84,10 @@ func StopScanArtifactCommand() *cobra.Command {
|
|||
var projectName, repoName, reference string
|
||||
|
||||
if len(args) > 0 {
|
||||
projectName, repoName, reference = utils.ParseProjectRepoReference(args[0])
|
||||
projectName, repoName, reference, err = utils.ParseProjectRepoReference(args[0])
|
||||
if err != nil {
|
||||
log.Errorf("failed to parse project/repo/reference: %v", err)
|
||||
}
|
||||
} else {
|
||||
var projectName string
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
|
|
|
@ -48,23 +48,24 @@ func CreateTagsCmd() *cobra.Command {
|
|||
Example: `harbor artifact tags create <project>/<repository>/<reference> <tag>`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
|
||||
if len(args) > 0 {
|
||||
projectName, repoName, reference := utils.ParseProjectRepoReference(args[0])
|
||||
tag := args[1]
|
||||
err = api.CreateTag(projectName, repoName, reference, tag)
|
||||
} else {
|
||||
var projectName, repoName, reference string
|
||||
var tagName string
|
||||
var projectName string
|
||||
if len(args) > 0 {
|
||||
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, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
repoName := prompt.GetRepoNameFromUser(projectName)
|
||||
reference := prompt.GetReferenceFromUser(repoName, projectName)
|
||||
repoName = prompt.GetRepoNameFromUser(projectName)
|
||||
reference = prompt.GetReferenceFromUser(repoName, projectName)
|
||||
create.CreateTagView(&tagName)
|
||||
err = api.CreateTag(projectName, repoName, reference, tagName)
|
||||
}
|
||||
err = api.CreateTag(projectName, repoName, reference, tagName)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create tag: %v", err)
|
||||
}
|
||||
|
@ -85,13 +86,19 @@ func ListTagsCmd() *cobra.Command {
|
|||
var projectName, repoName, reference string
|
||||
|
||||
if len(args) > 0 {
|
||||
projectName, repoName, reference = utils.ParseProjectRepoReference(args[0])
|
||||
projectName, repoName, reference, err = utils.ParseProjectRepoReference(args[0])
|
||||
if err != nil {
|
||||
log.Errorf("failed to parse project/repo/reference: %v", err)
|
||||
}
|
||||
} else {
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
repoName = prompt.GetRepoNameFromUser(projectName)
|
||||
if repoName == "" {
|
||||
return
|
||||
}
|
||||
reference = prompt.GetReferenceFromUser(repoName, projectName)
|
||||
}
|
||||
|
||||
|
@ -125,22 +132,24 @@ func DeleteTagsCmd() *cobra.Command {
|
|||
Example: `harbor artifact tags delete <project>/<repository>/<reference> <tag>`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
|
||||
var projectName, repoName, reference string
|
||||
var tagName string
|
||||
if len(args) > 0 {
|
||||
projectName, repoName, reference := utils.ParseProjectRepoReference(args[0])
|
||||
tag := args[1]
|
||||
err = api.DeleteTag(projectName, repoName, reference, tag)
|
||||
projectName, repoName, reference, err = utils.ParseProjectRepoReference(args[0])
|
||||
if err != nil {
|
||||
log.Errorf("failed to parse project/repo/reference: %v", err)
|
||||
}
|
||||
tagName = args[1]
|
||||
} else {
|
||||
var projectName string
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
repoName := prompt.GetRepoNameFromUser(projectName)
|
||||
reference := prompt.GetReferenceFromUser(repoName, projectName)
|
||||
tag := prompt.GetTagFromUser(repoName, projectName, reference)
|
||||
err = api.DeleteTag(projectName, repoName, reference, tag)
|
||||
repoName = prompt.GetRepoNameFromUser(projectName)
|
||||
reference = prompt.GetReferenceFromUser(repoName, projectName)
|
||||
tagName = prompt.GetTagFromUser(repoName, projectName, reference)
|
||||
}
|
||||
err = api.DeleteTag(projectName, repoName, reference, tagName)
|
||||
if err != nil {
|
||||
log.Errorf("failed to delete tag: %v", err)
|
||||
}
|
||||
|
|
|
@ -36,7 +36,10 @@ func ViewArtifactCommmand() *cobra.Command {
|
|||
var artifact *artifact.GetArtifactOK
|
||||
|
||||
if len(args) > 0 {
|
||||
projectName, repoName, reference = utils.ParseProjectRepoReference(args[0])
|
||||
projectName, repoName, reference, err = utils.ParseProjectRepoReference(args[0])
|
||||
if err != nil {
|
||||
log.Errorf("failed to parse project/repo/reference: %v", err)
|
||||
}
|
||||
} else {
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
|
@ -55,7 +58,7 @@ func ViewArtifactCommmand() *cobra.Command {
|
|||
}
|
||||
}
|
||||
|
||||
artifact, err = api.ViewArtifact(projectName, repoName, reference)
|
||||
artifact, err = api.ViewArtifact(projectName, repoName, reference, false)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("failed to get info of an artifact: %v", err)
|
||||
|
|
|
@ -19,14 +19,17 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/artifact"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/config"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/context"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/cve"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/instance"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/labels"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/project"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/quota"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/registry"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/replication"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/repository"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/robot"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/scan_all"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/scanner"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/schedule"
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/tag"
|
||||
|
@ -93,26 +96,102 @@ harbor help
|
|||
fmt.Println(err.Error())
|
||||
}
|
||||
|
||||
root.AddCommand(
|
||||
versionCommand(),
|
||||
LoginCommand(),
|
||||
config.Config(),
|
||||
HealthCommand(),
|
||||
project.Project(),
|
||||
registry.Registry(),
|
||||
repository.Repository(),
|
||||
user.User(),
|
||||
artifact.Artifact(),
|
||||
scanner.Scanner(),
|
||||
tag.TagCommand(),
|
||||
cve.CVEAllowlist(),
|
||||
schedule.Schedule(),
|
||||
labels.Labels(),
|
||||
InfoCommand(),
|
||||
webhook.Webhook(),
|
||||
instance.Instance(),
|
||||
quota.Quota(),
|
||||
)
|
||||
root.AddGroup(&cobra.Group{ID: "core", Title: "Core:"})
|
||||
root.AddGroup(&cobra.Group{ID: "access", Title: "Access:"})
|
||||
root.AddGroup(&cobra.Group{ID: "system", Title: "System:"})
|
||||
root.AddGroup(&cobra.Group{ID: "utils", Title: "Utility:"})
|
||||
|
||||
// Core
|
||||
cmd := InfoCommand()
|
||||
cmd.GroupID = "core"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = project.Project()
|
||||
cmd.GroupID = "core"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = repository.Repository()
|
||||
cmd.GroupID = "core"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = artifact.Artifact()
|
||||
cmd.GroupID = "core"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = tag.TagCommand()
|
||||
cmd.GroupID = "core"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = labels.Labels()
|
||||
cmd.GroupID = "core"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = quota.Quota()
|
||||
cmd.GroupID = "core"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = cve.CVEAllowlist()
|
||||
cmd.GroupID = "core"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = webhook.Webhook()
|
||||
cmd.GroupID = "core"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = robot.Robot()
|
||||
cmd.GroupID = "core"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
// Access
|
||||
cmd = LoginCommand()
|
||||
cmd.GroupID = "access"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = user.User()
|
||||
cmd.GroupID = "access"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
// System
|
||||
cmd = context.Context()
|
||||
cmd.GroupID = "system"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = HealthCommand()
|
||||
cmd.GroupID = "system"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = instance.Instance()
|
||||
cmd.GroupID = "system"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = registry.Registry()
|
||||
cmd.GroupID = "system"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = replication.Replication()
|
||||
cmd.GroupID = "system"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = scanner.Scanner()
|
||||
cmd.GroupID = "system"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = scan_all.ScanAll()
|
||||
cmd.GroupID = "system"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = schedule.Schedule()
|
||||
cmd.GroupID = "system"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
// Utils
|
||||
cmd = versionCommand()
|
||||
cmd.GroupID = "utils"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
cmd = Logs()
|
||||
cmd.GroupID = "utils"
|
||||
root.AddCommand(cmd)
|
||||
|
||||
return root
|
||||
}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package context
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
func Context() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "context",
|
||||
Short: "Manage locally available contexts",
|
||||
Example: "harbor context list",
|
||||
Long: `The context command allows you to manage configuration items of the Harbor CLI.
|
||||
You can add, get, or delete specific configuration items, as well as list all configuration items of the Harbor CLI.`,
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
ListContextCommand(),
|
||||
GetContextItemCommand(),
|
||||
UpdateContextItemCommand(),
|
||||
DeleteContextItemCommand(),
|
||||
SwitchContextCommand(),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -11,43 +11,44 @@
|
|||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package e2e
|
||||
package context_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
helpers "github.com/goharbor/harbor-cli/test/helper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_ConfigCmd(t *testing.T) {
|
||||
func Test_ContextCmd(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := Initialize(t, tempDir)
|
||||
defer ConfigCleanup(t, data)
|
||||
SetMockKeyring(t)
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"config"})
|
||||
rootCmd.SetArgs([]string{"context"})
|
||||
err := rootCmd.Execute()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func Test_ConfigListCmd(t *testing.T) {
|
||||
func Test_ContextListCmd(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := Initialize(t, tempDir)
|
||||
defer ConfigCleanup(t, data)
|
||||
SetMockKeyring(t)
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"config", "list"})
|
||||
rootCmd.SetArgs([]string{"context", "list"})
|
||||
err := rootCmd.Execute()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func Test_ConfigGetCmd_Success(t *testing.T) {
|
||||
func Test_ContextGetCmd_Success(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := Initialize(t, tempDir)
|
||||
defer ConfigCleanup(t, data)
|
||||
SetMockKeyring(t)
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
testConfig := &utils.HarborConfig{
|
||||
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
|
||||
Credentials: []utils.Credential{
|
||||
|
@ -64,16 +65,16 @@ func Test_ConfigGetCmd_Success(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"config", "get", "credentials.serveraddress"})
|
||||
rootCmd.SetArgs([]string{"context", "get", "credentials.serveraddress"})
|
||||
err = rootCmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_ConfigGetCmd_Failure(t *testing.T) {
|
||||
func Test_ContextGetCmd_Failure(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := Initialize(t, tempDir)
|
||||
defer ConfigCleanup(t, data)
|
||||
SetMockKeyring(t)
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
testConfig := &utils.HarborConfig{
|
||||
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
|
||||
Credentials: []utils.Credential{
|
||||
|
@ -90,16 +91,16 @@ func Test_ConfigGetCmd_Failure(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"config", "get", "serveraddress"})
|
||||
rootCmd.SetArgs([]string{"context", "get", "serveraddress"})
|
||||
err = rootCmd.Execute()
|
||||
assert.Error(t, err, "Expected an error when getting a non-existent config item")
|
||||
}
|
||||
|
||||
func Test_ConfigGetCmd_CredentialName_Success(t *testing.T) {
|
||||
func Test_ContextGetCmd_CredentialName_Success(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := Initialize(t, tempDir)
|
||||
defer ConfigCleanup(t, data)
|
||||
SetMockKeyring(t)
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
testConfig := &utils.HarborConfig{
|
||||
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
|
||||
Credentials: []utils.Credential{
|
||||
|
@ -116,16 +117,16 @@ func Test_ConfigGetCmd_CredentialName_Success(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"config", "get", "credentials.serveraddress", "--name", "harbor-cli@http://demo.goharbor.io"})
|
||||
rootCmd.SetArgs([]string{"context", "get", "credentials.serveraddress", "--name", "harbor-cli@http://demo.goharbor.io"})
|
||||
err = rootCmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_ConfigGetCmd_CredentialName_Failure(t *testing.T) {
|
||||
func Test_ContextGetCmd_CredentialName_Failure(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := Initialize(t, tempDir)
|
||||
defer ConfigCleanup(t, data)
|
||||
SetMockKeyring(t)
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
testConfig := &utils.HarborConfig{
|
||||
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
|
||||
Credentials: []utils.Credential{
|
||||
|
@ -142,16 +143,16 @@ func Test_ConfigGetCmd_CredentialName_Failure(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"config", "get", "credentials.serveraddress", "--name", "harbor-cli@http://goharbor.io"})
|
||||
rootCmd.SetArgs([]string{"context", "get", "credentials.serveraddress", "--name", "harbor-cli@http://goharbor.io"})
|
||||
err = rootCmd.Execute()
|
||||
assert.Error(t, err, "Expected an error when getting a non-existent credential name")
|
||||
}
|
||||
|
||||
func Test_ConfigUpdateCmd_Success(t *testing.T) {
|
||||
func Test_ContextUpdateCmd_Success(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := Initialize(t, tempDir)
|
||||
defer ConfigCleanup(t, data)
|
||||
SetMockKeyring(t)
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
testConfig := &utils.HarborConfig{
|
||||
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
|
||||
Credentials: []utils.Credential{
|
||||
|
@ -168,16 +169,16 @@ func Test_ConfigUpdateCmd_Success(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"config", "update", "credentials.serveraddress", "http://demo.goharbor.io"})
|
||||
rootCmd.SetArgs([]string{"context", "update", "credentials.serveraddress", "http://demo.goharbor.io"})
|
||||
err = rootCmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_ConfigUpdateCmd_CredentialName_Success(t *testing.T) {
|
||||
func Test_ContextUpdateCmd_CredentialName_Success(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := Initialize(t, tempDir)
|
||||
defer ConfigCleanup(t, data)
|
||||
SetMockKeyring(t)
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
testConfig := &utils.HarborConfig{
|
||||
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
|
||||
Credentials: []utils.Credential{
|
||||
|
@ -194,16 +195,16 @@ func Test_ConfigUpdateCmd_CredentialName_Success(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"config", "update", "credentials.serveraddress", "http://demo.goharbor.io", "--name", "harbor-cli@http://demo.goharbor.io"})
|
||||
rootCmd.SetArgs([]string{"context", "update", "credentials.serveraddress", "http://demo.goharbor.io", "--name", "harbor-cli@http://demo.goharbor.io"})
|
||||
err = rootCmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_ConfigUpdateCmd_CredentialName_Failure(t *testing.T) {
|
||||
func Test_ContextUpdateCmd_CredentialName_Failure(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := Initialize(t, tempDir)
|
||||
defer ConfigCleanup(t, data)
|
||||
SetMockKeyring(t)
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
testConfig := &utils.HarborConfig{
|
||||
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
|
||||
Credentials: []utils.Credential{
|
||||
|
@ -220,16 +221,16 @@ func Test_ConfigUpdateCmd_CredentialName_Failure(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"config", "update", "credentials.serveraddress", "http://demo.goharbor.io", "--name", "harbor-cli@http://goharbor.io"})
|
||||
rootCmd.SetArgs([]string{"context", "update", "credentials.serveraddress", "http://demo.goharbor.io", "--name", "harbor-cli@http://goharbor.io"})
|
||||
err = rootCmd.Execute()
|
||||
assert.Error(t, err, "Expected an error when setting a non-existent credential name")
|
||||
}
|
||||
|
||||
func Test_ConfigUpdateCmd_Failure(t *testing.T) {
|
||||
func Test_ContextUpdateCmd_Failure(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := Initialize(t, tempDir)
|
||||
defer ConfigCleanup(t, data)
|
||||
SetMockKeyring(t)
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
testConfig := &utils.HarborConfig{
|
||||
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
|
||||
Credentials: []utils.Credential{
|
||||
|
@ -246,16 +247,16 @@ func Test_ConfigUpdateCmd_Failure(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"config", "update", "serveraddress", "http://demo.goharbor.io"})
|
||||
rootCmd.SetArgs([]string{"context", "update", "serveraddress", "http://demo.goharbor.io"})
|
||||
err = rootCmd.Execute()
|
||||
assert.Error(t, err, "Expected an error when setting a non-existent config item")
|
||||
}
|
||||
|
||||
func Test_ConfigDeleteCmd_Success(t *testing.T) {
|
||||
func Test_ContextDeleteCmd_Success(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := Initialize(t, tempDir)
|
||||
defer ConfigCleanup(t, data)
|
||||
SetMockKeyring(t)
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
testConfig := &utils.HarborConfig{
|
||||
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
|
||||
Credentials: []utils.Credential{
|
||||
|
@ -272,7 +273,7 @@ func Test_ConfigDeleteCmd_Success(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"config", "delete", "credentials.serveraddress"})
|
||||
rootCmd.SetArgs([]string{"context", "delete", "credentials.serveraddress"})
|
||||
err = rootCmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
config, err := utils.GetCurrentHarborConfig()
|
||||
|
@ -282,11 +283,11 @@ func Test_ConfigDeleteCmd_Success(t *testing.T) {
|
|||
assert.Empty(t, config.Credentials[0].ServerAddress)
|
||||
}
|
||||
|
||||
func Test_ConfigDeleteCmd_Failure(t *testing.T) {
|
||||
func Test_ContextDeleteCmd_Failure(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := Initialize(t, tempDir)
|
||||
defer ConfigCleanup(t, data)
|
||||
SetMockKeyring(t)
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
testConfig := &utils.HarborConfig{
|
||||
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
|
||||
Credentials: []utils.Credential{
|
||||
|
@ -303,16 +304,16 @@ func Test_ConfigDeleteCmd_Failure(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"config", "delete", "serveraddress"})
|
||||
rootCmd.SetArgs([]string{"context", "delete", "serveraddress"})
|
||||
err = rootCmd.Execute()
|
||||
assert.Error(t, err, "Expected an error when deleting a non-existent config item")
|
||||
}
|
||||
|
||||
func Test_ConfigDeleteCmd_CredentialName_Success(t *testing.T) {
|
||||
func Test_ContextDeleteCmd_CredentialName_Success(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := Initialize(t, tempDir)
|
||||
defer ConfigCleanup(t, data)
|
||||
SetMockKeyring(t)
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
testConfig := &utils.HarborConfig{
|
||||
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
|
||||
Credentials: []utils.Credential{
|
||||
|
@ -329,7 +330,7 @@ func Test_ConfigDeleteCmd_CredentialName_Success(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"config", "delete", "credentials.serveraddress", "--name", "harbor-cli@http://demo.goharbor.io"})
|
||||
rootCmd.SetArgs([]string{"context", "delete", "credentials.serveraddress", "--name", "harbor-cli@http://demo.goharbor.io"})
|
||||
err = rootCmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
config, err := utils.GetCurrentHarborConfig()
|
||||
|
@ -339,11 +340,11 @@ func Test_ConfigDeleteCmd_CredentialName_Success(t *testing.T) {
|
|||
assert.Empty(t, config.Credentials[0].ServerAddress)
|
||||
}
|
||||
|
||||
func Test_ConfigDeleteCmd_CredentialName_Failure(t *testing.T) {
|
||||
func Test_ContextDeleteCmd_CredentialName_Failure(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := Initialize(t, tempDir)
|
||||
defer ConfigCleanup(t, data)
|
||||
SetMockKeyring(t)
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
testConfig := &utils.HarborConfig{
|
||||
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
|
||||
Credentials: []utils.Credential{
|
||||
|
@ -360,16 +361,16 @@ func Test_ConfigDeleteCmd_CredentialName_Failure(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"config", "delete", "credentials.serveraddress", "--name", "harbor-cli@http://goharbor.io"})
|
||||
rootCmd.SetArgs([]string{"context", "delete", "credentials.serveraddress", "--name", "harbor-cli@http://goharbor.io"})
|
||||
err = rootCmd.Execute()
|
||||
assert.Error(t, err, "Expected an error when deleting a non-existent credential name")
|
||||
}
|
||||
|
||||
func Test_ConfigDeleteCmd_Current_Flag_Success(t *testing.T) {
|
||||
func Test_ContextDeleteCmd_Current_Flag_Success(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := Initialize(t, tempDir)
|
||||
defer ConfigCleanup(t, data)
|
||||
SetMockKeyring(t)
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
testConfig := &utils.HarborConfig{
|
||||
CurrentCredentialName: "harbor-cli@http://demo.goharbor.io",
|
||||
Credentials: []utils.Credential{
|
||||
|
@ -392,7 +393,7 @@ func Test_ConfigDeleteCmd_Current_Flag_Success(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"config", "delete", "--current"})
|
||||
rootCmd.SetArgs([]string{"context", "delete", "--current"})
|
||||
err = rootCmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
config, err := utils.GetCurrentHarborConfig()
|
||||
|
@ -404,13 +405,13 @@ func Test_ConfigDeleteCmd_Current_Flag_Success(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_ConfigDeleteCmd_Current_Flag_With_Item_Failure(t *testing.T) {
|
||||
func Test_ContextDeleteCmd_Current_Flag_With_Item_Failure(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := Initialize(t, tempDir)
|
||||
defer ConfigCleanup(t, data)
|
||||
SetMockKeyring(t)
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
helpers.SetMockKeyring(t)
|
||||
rootCmd := root.RootCmd()
|
||||
rootCmd.SetArgs([]string{"config", "delete", "credentials.serveraddress", "--current"})
|
||||
rootCmd.SetArgs([]string{"context", "delete", "credentials.serveraddress", "--current"})
|
||||
err := rootCmd.Execute()
|
||||
assert.Error(t, err, "Expected an error when specifying both --current and an item")
|
||||
}
|
|
@ -11,7 +11,7 @@
|
|||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package config
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -25,9 +25,9 @@ import (
|
|||
|
||||
var deleteCurrent bool
|
||||
|
||||
// DeleteConfigItemCommand creates the 'harbor config delete' subcommand,
|
||||
// allowing you to do: harbor config delete <item>
|
||||
func DeleteConfigItemCommand() *cobra.Command {
|
||||
// DeleteContextItemCommand creates the 'harbor context delete' subcommand,
|
||||
// allowing you to do: harbor context delete <item>
|
||||
func DeleteContextItemCommand() *cobra.Command {
|
||||
var credentialName string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
@ -35,13 +35,13 @@ func DeleteConfigItemCommand() *cobra.Command {
|
|||
Short: "Delete (clear) a specific config item",
|
||||
Example: `
|
||||
# Clear the current credential's password
|
||||
harbor config delete credentials.password
|
||||
harbor context delete credentials.password
|
||||
|
||||
# Clear a specific credential's password using --name
|
||||
harbor config delete credentials.password --name admin@http://demo.goharbor.io
|
||||
harbor context delete credentials.password --name admin@http://demo.goharbor.io
|
||||
|
||||
# Clear the current credential
|
||||
harbor config delete --current
|
||||
harbor context delete --current
|
||||
`,
|
||||
Long: `Clear the value of a specific CLI config item by setting it to its zero value.
|
||||
Case-insensitive field lookup, but uses the canonical (Go) field name internally.
|
|
@ -11,7 +11,7 @@
|
|||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package config
|
||||
package context
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
@ -26,7 +26,7 @@ import (
|
|||
)
|
||||
|
||||
// GetConfigItemCommand creates the 'harbor config get' subcommand.
|
||||
func GetConfigItemCommand() *cobra.Command {
|
||||
func GetContextItemCommand() *cobra.Command {
|
||||
var credentialName string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
@ -34,7 +34,7 @@ func GetConfigItemCommand() *cobra.Command {
|
|||
Short: "Get a specific config item",
|
||||
Example: `
|
||||
# Get the current credential's username
|
||||
harbor config get credentials.username
|
||||
harbor context get credentials.username
|
||||
|
||||
# Get a credential's username by specifying the credential name
|
||||
harbor config get credentials.username --name admin@http://demo.goharbor.io
|
|
@ -11,29 +11,29 @@
|
|||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package config
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/context/list"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func ListConfigCommand() *cobra.Command {
|
||||
func ListContextCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List config items",
|
||||
Example: ` harbor config list`,
|
||||
Long: `Get information of all CLI config items`,
|
||||
Short: "List contexts",
|
||||
Example: ` harbor context list`,
|
||||
Args: cobra.MaximumNArgs(0),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
config, err := utils.GetCurrentHarborConfig()
|
||||
if err != nil {
|
||||
logrus.Errorf("Failed to get config: %v", err)
|
||||
fmt.Println("failed to get config: ", utils.ParseHarborErrorMsg(err))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -43,19 +43,19 @@ func ListConfigCommand() *cobra.Command {
|
|||
// Use utils.PrintFormat if available
|
||||
err = utils.PrintFormat(config, formatFlag)
|
||||
if err != nil {
|
||||
logrus.Errorf("Failed to print config: %v", err)
|
||||
}
|
||||
} else {
|
||||
// Default to YAML format
|
||||
data, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
logrus.Errorf("Failed to marshal config to YAML: %v", err)
|
||||
fmt.Println("Failed to print config: ", utils.ParseHarborErrorMsg(err))
|
||||
return
|
||||
}
|
||||
fmt.Println(string(data))
|
||||
} else {
|
||||
var cxlist []api.ContextListView
|
||||
for _, cred := range config.Credentials {
|
||||
cx := api.ContextListView{Name: cred.Name, Username: cred.Username, Server: cred.ServerAddress}
|
||||
cxlist = append(cxlist, cx)
|
||||
}
|
||||
currentCredential := config.CurrentCredentialName
|
||||
list.ListContexts(cxlist, currentCredential)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func SwitchContextCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "switch <none|context>",
|
||||
Short: "Switch to a new context",
|
||||
Example: `harbor context switch harbor-cli@https-demo-goharbor-io`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
config, err := utils.GetCurrentHarborConfig()
|
||||
if err != nil {
|
||||
fmt.Println("failed to get config: ", utils.ParseHarborErrorMsg(err))
|
||||
return
|
||||
}
|
||||
|
||||
if len(args) == 1 {
|
||||
newActiveCredential := args[0]
|
||||
found := false
|
||||
|
||||
for _, cred := range config.Credentials {
|
||||
if cred.Name == newActiveCredential {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found {
|
||||
config.CurrentCredentialName = newActiveCredential
|
||||
if err := utils.UpdateConfigFile(config); err != nil {
|
||||
fmt.Println("failed to update config: ", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
} else {
|
||||
fmt.Println("context doesn't exist")
|
||||
}
|
||||
} else {
|
||||
res, err := prompt.GetActiveContextFromUser()
|
||||
if err != nil {
|
||||
fmt.Println("failed to get active context: ", utils.ParseHarborErrorMsg(err))
|
||||
return
|
||||
}
|
||||
if res != "" {
|
||||
msg := fmt.Sprintf("context switched from '%s' to '%s'", config.CurrentCredentialName, res)
|
||||
config.CurrentCredentialName = res
|
||||
if err := utils.UpdateConfigFile(config); err != nil {
|
||||
fmt.Println("failed to update config: ", utils.ParseHarborErrorMsg(err))
|
||||
} else {
|
||||
fmt.Println(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
|
@ -11,7 +11,7 @@
|
|||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package config
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -26,7 +26,7 @@ import (
|
|||
|
||||
// UpdateConfigItemCommand creates the 'harbor config update' subcommand,
|
||||
// allowing you to do: harbor config update <item> <value>.
|
||||
func UpdateConfigItemCommand() *cobra.Command {
|
||||
func UpdateContextItemCommand() *cobra.Command {
|
||||
var credentialName string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
@ -34,7 +34,7 @@ func UpdateConfigItemCommand() *cobra.Command {
|
|||
Short: "Set/update a specific config item",
|
||||
Example: `
|
||||
# Set/update the current credential's password
|
||||
harbor config update credentials.password myNewSecret
|
||||
harbor context update credentials.password myNewSecret
|
||||
|
||||
# Set/update a credential's password by specifying the credential name
|
||||
harbor config update credentials.password myNewSecret --name admin@http://demo.goharbor.io
|
|
@ -14,10 +14,12 @@
|
|||
package labels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
@ -28,22 +30,27 @@ func DeleteLabelCommand() *cobra.Command {
|
|||
Short: "delete label",
|
||||
Example: "harbor label delete [labelname]",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
var labelId int64
|
||||
deleteView := &api.ListFlags{
|
||||
Scope: opts.Scope,
|
||||
}
|
||||
|
||||
if len(args) > 0 {
|
||||
labelId, _ := api.GetLabelIdByName(args[0])
|
||||
err = api.DeleteLabel(labelId)
|
||||
labelId, _ = api.GetLabelIdByName(args[0])
|
||||
} else {
|
||||
labelId := prompt.GetLabelIdFromUser(*deleteView)
|
||||
err = api.DeleteLabel(labelId)
|
||||
}
|
||||
labelList, err := api.ListLabel(*deleteView)
|
||||
if err != nil {
|
||||
log.Errorf("failed to delete label: %v", err)
|
||||
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()
|
||||
|
|
|
@ -40,7 +40,12 @@ func UpdateLableCommand() *cobra.Command {
|
|||
if len(args) > 0 {
|
||||
labelId, err = api.GetLabelIdByName(args[0])
|
||||
} else {
|
||||
labelId = prompt.GetLabelIdFromUser(updateflags)
|
||||
labelList, err := api.ListLabel(updateflags)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get label list: %v", err)
|
||||
return
|
||||
}
|
||||
labelId = prompt.GetLabelIdFromUser(labelList.Payload)
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("failed to parse label id: %v", err)
|
||||
|
|
|
@ -78,7 +78,6 @@ func LoginCommand() *cobra.Command {
|
|||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&Name, "name", "", "", "name for the set of credentials")
|
||||
flags.StringVarP(&Username, "username", "u", "", "Username")
|
||||
flags.StringVarP(&Password, "password", "p", "", "Password")
|
||||
flags.BoolVar(&passwordStdin, "password-stdin", false, "Take the password from stdin")
|
||||
|
@ -88,22 +87,14 @@ func LoginCommand() *cobra.Command {
|
|||
|
||||
// ProcessLogin applies a simplified decision logic to run login or launch an interactive view.
|
||||
func ProcessLogin(loginView login.LoginView, config *utils.HarborConfig) error {
|
||||
// Auto-generate the name if not provided.
|
||||
if loginView.Name == "" && loginView.Server != "" && loginView.Username != "" {
|
||||
// Auto-generate the name
|
||||
loginView.Name = fmt.Sprintf("%s@%s", loginView.Username, utils.SanitizeServerAddress(loginView.Server))
|
||||
}
|
||||
|
||||
// If complete credentials are provided (overrides), run login using them directly.
|
||||
if loginView.Server != "" && loginView.Username != "" && loginView.Password != "" {
|
||||
return RunLogin(loginView)
|
||||
}
|
||||
// If a name is provided, try to load the matching credential from the config.
|
||||
if loginView.Name != "" {
|
||||
loadedLoginView, err := LoadCredentialsIntoLoginView(loginView.Name, config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load credentials: %w", err)
|
||||
}
|
||||
return RunLogin(loadedLoginView)
|
||||
}
|
||||
|
||||
// If nothing matches, launch the interactive view.
|
||||
return CreateLoginView(&loginView)
|
||||
}
|
||||
|
@ -124,29 +115,6 @@ func CreateLoginView(loginView *login.LoginView) error {
|
|||
return RunLogin(*loginView)
|
||||
}
|
||||
|
||||
// LoadCredentialsIntoLoginView loads a stored credential from the config by name and returns a LoginView.
|
||||
func LoadCredentialsIntoLoginView(credentialName string, config *utils.HarborConfig) (login.LoginView, error) {
|
||||
for _, cred := range config.Credentials {
|
||||
if cred.Name == credentialName {
|
||||
key, err := utils.GetEncryptionKey()
|
||||
if err != nil {
|
||||
return login.LoginView{}, fmt.Errorf("failed to get encryption key: %w", err)
|
||||
}
|
||||
decryptedPassword, err := utils.Decrypt(key, string(cred.Password))
|
||||
if err != nil {
|
||||
return login.LoginView{}, fmt.Errorf("failed to decrypt password: %w", err)
|
||||
}
|
||||
return login.LoginView{
|
||||
Server: cred.ServerAddress,
|
||||
Username: cred.Username,
|
||||
Password: decryptedPassword,
|
||||
Name: cred.Name,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return login.LoginView{}, fmt.Errorf("credential with name %s not found", credentialName)
|
||||
}
|
||||
|
||||
// RunLogin attempts to log in using the provided LoginView credentials.
|
||||
func RunLogin(opts login.LoginView) error {
|
||||
opts.Server = utils.FormatUrl(opts.Server)
|
||||
|
@ -156,11 +124,15 @@ func RunLogin(opts login.LoginView) error {
|
|||
Username: opts.Username,
|
||||
Password: opts.Password,
|
||||
}
|
||||
err := utils.ValidateURL(opts.Server)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid server URL: %s", err)
|
||||
}
|
||||
client := utils.GetClientByConfig(clientConfig)
|
||||
ctx := context.Background()
|
||||
_, err := client.User.GetCurrentUserInfo(ctx, &user.GetCurrentUserInfoParams{})
|
||||
_, err = client.User.GetCurrentUserInfo(ctx, &user.GetCurrentUserInfoParams{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("login failed, please check your credentials: %s", err)
|
||||
return fmt.Errorf("%v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
if err := utils.GenerateEncryptionKey(); err != nil {
|
||||
fmt.Println("Encryption key already exists or could not be created:", err)
|
||||
|
@ -195,12 +167,14 @@ func RunLogin(opts login.LoginView) error {
|
|||
if existingCred.Username == opts.Username && existingCred.ServerAddress == opts.Server {
|
||||
if existingCred.Password == encryptedPassword {
|
||||
log.Warn("Credentials already exist in the config file. They were not added again.")
|
||||
fmt.Printf("Login successful for %s at %s\n", opts.Username, opts.Server)
|
||||
return nil
|
||||
} else {
|
||||
log.Warn("Credentials already exist in the config file but the password is different. Updating the password.")
|
||||
if err = utils.UpdateCredentialsInConfigFile(cred, configPath); err != nil {
|
||||
log.Fatalf("failed to update the credential: %s", err)
|
||||
}
|
||||
fmt.Printf("Login successful for %s at %s\n", opts.Username, opts.Server)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
|
@ -208,6 +182,7 @@ func RunLogin(opts login.LoginView) error {
|
|||
if err = utils.UpdateCredentialsInConfigFile(cred, configPath); err != nil {
|
||||
log.Fatalf("failed to update the credential: %s", err)
|
||||
}
|
||||
fmt.Printf("Login successful for %s at %s\n", opts.Username, opts.Server)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -216,5 +191,6 @@ func RunLogin(opts login.LoginView) error {
|
|||
return fmt.Errorf("failed to store the credential: %s", err)
|
||||
}
|
||||
log.Debugf("Credentials successfully added to the config file.")
|
||||
fmt.Printf("Login successful for %s at %s\n", opts.Username, opts.Server)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -11,54 +11,50 @@
|
|||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package e2e
|
||||
package root_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root"
|
||||
helpers "github.com/goharbor/harbor-cli/test/helper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// func Test_Login_Success(t *testing.T) {
|
||||
// tempDir := t.TempDir()
|
||||
// data := Initialize(t, tempDir)
|
||||
// defer ConfigCleanup(t, data)
|
||||
func Test_Login_Success(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
cmd := root.LoginCommand()
|
||||
validServerAddresses := []string{
|
||||
"http://demo.goharbor.io:80",
|
||||
"https://demo.goharbor.io:443",
|
||||
"http://demo.goharbor.io",
|
||||
"https://demo.goharbor.io",
|
||||
}
|
||||
|
||||
// SetMockKeyring(t)
|
||||
for _, serverAddress := range validServerAddresses {
|
||||
t.Run("ValidServer_"+serverAddress, func(t *testing.T) {
|
||||
args := []string{serverAddress}
|
||||
cmd.SetArgs(args)
|
||||
|
||||
// cmd := root.LoginCommand()
|
||||
// validServerAddresses := []string{
|
||||
// "http://demo.goharbor.io:80",
|
||||
// "https://demo.goharbor.io:443",
|
||||
// "http://demo.goharbor.io",
|
||||
// "https://demo.goharbor.io",
|
||||
// }
|
||||
assert.NoError(t, cmd.Flags().Set("username", "harbor-cli"))
|
||||
assert.NoError(t, cmd.Flags().Set("password", "Harbor12345"))
|
||||
|
||||
// for _, serverAddress := range validServerAddresses {
|
||||
// t.Run("ValidServer_"+serverAddress, func(t *testing.T) {
|
||||
// args := []string{serverAddress}
|
||||
// cmd.SetArgs(args)
|
||||
|
||||
// assert.NoError(t, cmd.Flags().Set("name", "test"))
|
||||
// assert.NoError(t, cmd.Flags().Set("username", "harbor-cli"))
|
||||
// assert.NoError(t, cmd.Flags().Set("password", "Harbor12345"))
|
||||
|
||||
// err := cmd.Execute()
|
||||
// assert.NoError(t, err, "Expected no error for server: %s", serverAddress)
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err, "Expected no error for server: %s", serverAddress)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Login_Failure_WrongServer(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := Initialize(t, tempDir)
|
||||
defer ConfigCleanup(t, data)
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
|
||||
cmd := root.LoginCommand()
|
||||
cmd.SetArgs([]string{"wrongserver"})
|
||||
|
||||
assert.NoError(t, cmd.Flags().Set("name", "test"))
|
||||
assert.NoError(t, cmd.Flags().Set("username", "harbor-cli"))
|
||||
assert.NoError(t, cmd.Flags().Set("password", "Harbor12345"))
|
||||
|
||||
|
@ -68,13 +64,12 @@ func Test_Login_Failure_WrongServer(t *testing.T) {
|
|||
|
||||
func Test_Login_Failure_WrongUsername(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := Initialize(t, tempDir)
|
||||
defer ConfigCleanup(t, data)
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
|
||||
cmd := root.LoginCommand()
|
||||
cmd.SetArgs([]string{"http://demo.goharbor.io"})
|
||||
|
||||
assert.NoError(t, cmd.Flags().Set("name", "test"))
|
||||
assert.NoError(t, cmd.Flags().Set("username", "does-not-exist"))
|
||||
assert.NoError(t, cmd.Flags().Set("password", "Harbor12345"))
|
||||
|
||||
|
@ -84,13 +79,12 @@ func Test_Login_Failure_WrongUsername(t *testing.T) {
|
|||
|
||||
func Test_Login_Failure_WrongPassword(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := Initialize(t, tempDir)
|
||||
defer ConfigCleanup(t, data)
|
||||
data := helpers.Initialize(t, tempDir)
|
||||
defer helpers.ConfigCleanup(t, data)
|
||||
|
||||
cmd := root.LoginCommand()
|
||||
cmd.SetArgs([]string{"http://demo.goharbor.io"})
|
||||
|
||||
assert.NoError(t, cmd.Flags().Set("name", "test"))
|
||||
assert.NoError(t, cmd.Flags().Set("username", "admin"))
|
||||
assert.NoError(t, cmd.Flags().Set("password", "wrong"))
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package root
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
list "github.com/goharbor/harbor-cli/pkg/views/logs"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var logsLogger = log.New()
|
||||
|
||||
func Logs() *cobra.Command {
|
||||
var opts api.ListFlags
|
||||
var follow bool
|
||||
var refreshInterval string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "logs",
|
||||
Short: "Get recent logs of the projects which the user is a member of",
|
||||
Args: cobra.NoArgs,
|
||||
Long: `Get recent logs of the projects which the user is a member of.
|
||||
This command retrieves the audit logs for the projects the user is a member of. It supports pagination, sorting, and filtering through query parameters. The logs can be followed in real-time with the --follow flag, and the output can be formatted as JSON with the --output-format flag.
|
||||
|
||||
harbor-cli logs --page 1 --page-size 10 --query "operation=push" --sort "op_time:desc"
|
||||
|
||||
harbor-cli logs --follow --refresh-interval 2s
|
||||
|
||||
harbor-cli logs --output-format json`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if refreshInterval != "" && !follow {
|
||||
fmt.Println("The --refresh-interval flag is only applicable when using --follow. It will be ignored.")
|
||||
}
|
||||
|
||||
if follow {
|
||||
var interval time.Duration = 5 * time.Second
|
||||
var err error
|
||||
if refreshInterval != "" {
|
||||
interval, err = time.ParseDuration(refreshInterval)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid refresh interval: %v", err)
|
||||
}
|
||||
}
|
||||
followLogs(opts, interval)
|
||||
} else {
|
||||
logs, err := api.AuditLogs(opts)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to retrieve audit logs: %v", err)
|
||||
}
|
||||
|
||||
formatFlag := viper.GetString("output-format")
|
||||
if formatFlag != "" {
|
||||
log.WithField("output_format", formatFlag).Debug("Output format selected")
|
||||
err = utils.PrintFormat(logs.Payload, formatFlag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
list.ListLogs(logs.Payload)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.Int64VarP(&opts.Page, "page", "", 1, "Page number")
|
||||
flags.Int64VarP(&opts.PageSize, "page-size", "", 10, "Size of per page")
|
||||
flags.StringVarP(&opts.Q, "query", "q", "", "Query string to query resources")
|
||||
flags.StringVarP(
|
||||
&opts.Sort,
|
||||
"sort",
|
||||
"",
|
||||
"",
|
||||
"Sort the resource list in ascending or descending order",
|
||||
)
|
||||
flags.BoolVarP(&follow, "follow", "f", false, "Follow log output (tail -f behavior)")
|
||||
flags.StringVarP(&refreshInterval, "refresh-interval", "n", "",
|
||||
"Interval to refresh logs when following (default: 5s)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func followLogs(opts api.ListFlags, interval time.Duration) {
|
||||
var lastLogTime *time.Time
|
||||
|
||||
logsLogger.SetFormatter(&log.TextFormatter{
|
||||
FullTimestamp: true,
|
||||
TimestampFormat: "2006-01-02 15:04:05",
|
||||
DisableColors: false,
|
||||
})
|
||||
logsLogger.SetLevel(log.InfoLevel)
|
||||
logsLogger.SetOutput(os.Stdout)
|
||||
|
||||
fmt.Println("Following Harbor audit logs... (Press Ctrl+C to stop)")
|
||||
|
||||
for {
|
||||
logs, err := api.AuditLogs(opts)
|
||||
if err != nil {
|
||||
log.Errorf("failed to retrieve audit logs: %v", err)
|
||||
time.Sleep(interval)
|
||||
continue
|
||||
}
|
||||
|
||||
var newLogs []*models.AuditLogExt
|
||||
if lastLogTime != nil {
|
||||
for _, logEntry := range logs.Payload {
|
||||
logTime := time.Time(logEntry.OpTime)
|
||||
if !logTime.IsZero() && logTime.After(*lastLogTime) {
|
||||
newLogs = append(newLogs, logEntry)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newLogs = logs.Payload
|
||||
}
|
||||
|
||||
if len(logs.Payload) > 0 {
|
||||
logTime := time.Time(logs.Payload[0].OpTime)
|
||||
if !logTime.IsZero() {
|
||||
lastLogTime = &logTime
|
||||
}
|
||||
}
|
||||
|
||||
printLogsAsStream(newLogs)
|
||||
time.Sleep(interval)
|
||||
}
|
||||
}
|
||||
|
||||
func printLogsAsStream(logs []*models.AuditLogExt) {
|
||||
for _, logEntry := range logs {
|
||||
logTime := time.Time(logEntry.OpTime)
|
||||
level := getLogLevel(logEntry.OperationResult)
|
||||
|
||||
displayUser := truncateUsername(logEntry.Username)
|
||||
resource := getResourceInfo(logEntry.ResourceType, logEntry.Resource)
|
||||
|
||||
resultIcon := "✓"
|
||||
if !logEntry.OperationResult {
|
||||
resultIcon = "✗"
|
||||
}
|
||||
|
||||
message := fmt.Sprintf("%s %s %s %s",
|
||||
displayUser,
|
||||
logEntry.Operation,
|
||||
resource,
|
||||
resultIcon)
|
||||
|
||||
entry := logsLogger.WithTime(logTime)
|
||||
|
||||
switch level {
|
||||
case "error":
|
||||
entry.Error(message)
|
||||
case "info":
|
||||
entry.Info(message)
|
||||
default:
|
||||
entry.Debug(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func truncateUsername(username string) string {
|
||||
if username == "" {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
if len(username) > 30 {
|
||||
if parts := strings.Split(username, "+"); len(parts) > 1 {
|
||||
project := strings.TrimPrefix(parts[0], "robt_")
|
||||
return fmt.Sprintf("%s+robot", project)
|
||||
}
|
||||
return username[:27] + "..."
|
||||
}
|
||||
return username
|
||||
}
|
||||
|
||||
func getLogLevel(operationResult bool) string {
|
||||
switch operationResult {
|
||||
case false:
|
||||
return "error"
|
||||
case true:
|
||||
return "info"
|
||||
default:
|
||||
return "error"
|
||||
}
|
||||
}
|
||||
|
||||
func getResourceInfo(resourceType, resource string) string {
|
||||
if resourceType == "" && resource == "" {
|
||||
return "unknown"
|
||||
}
|
||||
if resourceType != "" && resource != "" {
|
||||
return fmt.Sprintf("%s:%s", resourceType, resource)
|
||||
}
|
||||
if resourceType != "" {
|
||||
return resourceType
|
||||
}
|
||||
return resource
|
||||
}
|
|
@ -14,6 +14,7 @@
|
|||
package project
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor-cli/cmd/harbor/root/project/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
@ -30,7 +31,9 @@ func Project() *cobra.Command {
|
|||
ListProjectCommand(),
|
||||
ViewCommand(),
|
||||
LogsProjectCommmand(),
|
||||
config.ProjectConfigCommand(),
|
||||
SearchProjectCommand(),
|
||||
Robot(),
|
||||
)
|
||||
|
||||
return cmd
|
||||
|
|
|
@ -15,19 +15,18 @@ package config
|
|||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
func Config() *cobra.Command {
|
||||
var isID bool
|
||||
|
||||
func ProjectConfigCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Manage the config of the Harbor CLI",
|
||||
Long: `The config command allows you to manage configurations of the Harbor CLI.
|
||||
You can add, get, or delete specific config item, as well as list all config items of the Harbor Cli`,
|
||||
Short: "Manage project configuration",
|
||||
}
|
||||
cmd.AddCommand(
|
||||
ListConfigCommand(),
|
||||
GetConfigItemCommand(),
|
||||
UpdateConfigItemCommand(),
|
||||
DeleteConfigItemCommand(),
|
||||
UpdateProjectConfigCmd(),
|
||||
ListProjectConfigCmd(),
|
||||
)
|
||||
cmd.PersistentFlags().BoolVarP(&isID, "id", "", false, "Use project ID instead of name")
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -39,15 +39,19 @@ func CreateProjectCommand() *cobra.Command {
|
|||
}
|
||||
|
||||
if opts.ProxyCache && opts.RegistryID == "" {
|
||||
return fmt.Errorf("Error: Proxy cache selected but no registry ID provided. Use --registry-id.")
|
||||
return fmt.Errorf("proxy cache selected but no registry ID provided. Use --registry-id")
|
||||
}
|
||||
|
||||
if opts.ProjectName != "" {
|
||||
if !opts.ProxyCache && opts.RegistryID != "" {
|
||||
return fmt.Errorf("registry ID should only be provided when proxy-cache is enabled")
|
||||
}
|
||||
|
||||
if opts.ProjectName != "" && opts.StorageLimit != "" {
|
||||
log.Debug("Attempting to create project using flags...")
|
||||
err = api.CreateProject(opts)
|
||||
ProjectName = opts.ProjectName
|
||||
} else {
|
||||
log.Debug("No project name provided. Switching to interactive view...")
|
||||
log.Debug("Switching to interactive view...")
|
||||
createView := &create.CreateView{
|
||||
ProjectName: opts.ProjectName,
|
||||
Public: opts.Public,
|
||||
|
@ -71,7 +75,7 @@ func CreateProjectCommand() *cobra.Command {
|
|||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.Public, "public", "", false, "Project is public or private")
|
||||
flags.StringVarP(&opts.RegistryID, "registry-id", "", "", "ID of referenced registry when creating the proxy cache project")
|
||||
flags.StringVarP(&opts.StorageLimit, "storage-limit", "", "-1", "Storage quota of the project")
|
||||
flags.StringVarP(&opts.StorageLimit, "storage-limit", "", "", "Storage quota of the project")
|
||||
flags.BoolVarP(&opts.ProxyCache, "proxy-cache", "", false, "Whether the project is a proxy cache project")
|
||||
|
||||
return cmd
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -45,7 +45,11 @@ func UpdateRegistryCommand() *cobra.Command {
|
|||
registryId = prompt.GetRegistryNameFromUser()
|
||||
}
|
||||
|
||||
existingRegistry := api.GetRegistryResponse(registryId)
|
||||
existingRegistry, err := api.GetRegistryResponse(registryId)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get registry with ID %d: %v", registryId, err)
|
||||
return
|
||||
}
|
||||
if existingRegistry == nil {
|
||||
log.Errorf("registry is not found")
|
||||
return
|
||||
|
|
|
@ -22,10 +22,14 @@ func Replication() *cobra.Command {
|
|||
var replicationCmd = &cobra.Command{
|
||||
Use: "replication",
|
||||
Aliases: []string{"repl"},
|
||||
Short: "",
|
||||
Long: ``,
|
||||
Short: "Manage replications",
|
||||
Long: `Manage replications in Harbor context`,
|
||||
}
|
||||
replicationCmd.AddCommand()
|
||||
replicationCmd.AddCommand(
|
||||
ReplicationPoliciesCommand(),
|
||||
StartCommand(),
|
||||
StopCommand(),
|
||||
)
|
||||
|
||||
return replicationCmd
|
||||
}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package replication
|
||||
|
||||
import (
|
||||
rpolicies "github.com/goharbor/harbor-cli/cmd/harbor/root/replication/policies"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func ReplicationPoliciesCommand() *cobra.Command {
|
||||
// replicationCmd represents the replication command.
|
||||
var replicationCmd = &cobra.Command{
|
||||
Use: "policies",
|
||||
Aliases: []string{"pol"},
|
||||
Short: "Manage replication policies",
|
||||
Long: `Manage replication policies in Harbor context`,
|
||||
}
|
||||
replicationCmd.AddCommand(
|
||||
rpolicies.ListCommand(),
|
||||
rpolicies.ViewCommand(),
|
||||
rpolicies.DeleteCommand(),
|
||||
rpolicies.CreateCommand(),
|
||||
rpolicies.UpdateCommand(),
|
||||
)
|
||||
|
||||
return replicationCmd
|
||||
}
|
|
@ -0,0 +1,185 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package policies
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/goharbor/go-client/pkg/sdk/v2.0/client/replication"
|
||||
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
config "github.com/goharbor/harbor-cli/pkg/config/replication"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/replication/policies/create"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func CreateCommand() *cobra.Command {
|
||||
var configFile string
|
||||
var registryID int64
|
||||
var err error
|
||||
var opts *create.CreateView
|
||||
cmd := &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "create replication policies",
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
log.Debug("Starting replications create command")
|
||||
|
||||
if configFile != "" {
|
||||
log.Debugf("Loading replication policy configuration from file: %s", configFile)
|
||||
opts, err = config.LoadConfigFromFile(configFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load replication policy configuration: %v", err)
|
||||
}
|
||||
registryID, err = api.GetRegistryIdByName(opts.TargetRegistry)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get registry ID for name %s: %v", opts.TargetRegistry, err)
|
||||
}
|
||||
if registryID == 0 {
|
||||
return fmt.Errorf("registry with name %s not found", opts.TargetRegistry)
|
||||
}
|
||||
} else {
|
||||
opts = &create.CreateView{}
|
||||
create.CreateRPolicyView(opts, false)
|
||||
registryID = prompt.GetRegistryNameFromUser()
|
||||
}
|
||||
|
||||
registry, err := api.GetRegistryResponse(registryID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get registry with ID %d: %v", registryID, err)
|
||||
}
|
||||
|
||||
policy := ConvertToPolicy(opts, registry)
|
||||
response, err := api.CreateReplicationPolicy(&replication.CreateReplicationPolicyParams{
|
||||
Policy: policy,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create replication policy: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
fmt.Println("Replication policy created successfully with ID:", response.Location)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&configFile, "policy-config-file", "f", "", "YAML/JSON file with robot configuration")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func ConvertToPolicy(view *create.CreateView, registry *models.Registry) *models.ReplicationPolicy {
|
||||
policy := &models.ReplicationPolicy{
|
||||
Name: view.Name,
|
||||
Description: view.Description,
|
||||
Enabled: view.Enabled,
|
||||
Override: view.Override,
|
||||
// ReplicateDeletion is the favored field to use for deletion replication
|
||||
// Deletion is deprecated and will be removed in future versions
|
||||
// However, for updating from false to true, we need to set both fields
|
||||
ReplicateDeletion: view.ReplicateDeletion,
|
||||
Deletion: view.ReplicateDeletion,
|
||||
CopyByChunk: &view.CopyByChunk,
|
||||
Filters: []*models.ReplicationFilter{},
|
||||
}
|
||||
|
||||
if view.Speed != "" {
|
||||
speedInt, _ := strconv.ParseInt(view.Speed, 10, 32)
|
||||
speed := int32(speedInt)
|
||||
policy.Speed = &speed
|
||||
}
|
||||
|
||||
trigger := &models.ReplicationTrigger{
|
||||
Type: view.TriggerType,
|
||||
}
|
||||
if view.TriggerType == "scheduled" {
|
||||
trigger.TriggerSettings = &models.ReplicationTriggerSettings{
|
||||
Cron: view.CronString,
|
||||
}
|
||||
}
|
||||
policy.Trigger = trigger
|
||||
|
||||
if view.ReplicationMode == "Pull" {
|
||||
// Pull mode (external -> Harbor)
|
||||
policy.SrcRegistry = registry
|
||||
policy.DestRegistry = nil
|
||||
} else {
|
||||
// Push mode (Harbor -> external)
|
||||
policy.SrcRegistry = nil
|
||||
policy.DestRegistry = registry
|
||||
}
|
||||
|
||||
var resourceFilter *models.ReplicationFilter
|
||||
var nameFilter *models.ReplicationFilter
|
||||
var tagFilter *models.ReplicationFilter
|
||||
// var labelFilter *models.ReplicationFilter
|
||||
var filters []*models.ReplicationFilter
|
||||
|
||||
if view.ResourceFilter != "" {
|
||||
resourceFilter = &models.ReplicationFilter{
|
||||
Type: "resource",
|
||||
Value: view.ResourceFilter,
|
||||
Decoration: "",
|
||||
}
|
||||
filters = append(filters, resourceFilter)
|
||||
}
|
||||
|
||||
if view.NameFilter != "" {
|
||||
nameFilter = &models.ReplicationFilter{
|
||||
Type: "name",
|
||||
Value: view.NameFilter,
|
||||
Decoration: "",
|
||||
}
|
||||
filters = append(filters, nameFilter)
|
||||
}
|
||||
|
||||
if view.TagPattern != "" {
|
||||
tagFilter = &models.ReplicationFilter{
|
||||
Type: "tag",
|
||||
Value: view.TagPattern,
|
||||
Decoration: view.TagFilter,
|
||||
}
|
||||
filters = append(filters, tagFilter)
|
||||
}
|
||||
|
||||
if view.LabelPattern != "" {
|
||||
decoration := "matches"
|
||||
if view.LabelFilter == "excludes" {
|
||||
decoration = "excludes"
|
||||
}
|
||||
|
||||
var labelValues []string
|
||||
if strings.Contains(view.LabelPattern, ",") {
|
||||
labelValues = strings.Split(view.LabelPattern, ",")
|
||||
for i, label := range labelValues {
|
||||
labelValues[i] = strings.TrimSpace(label)
|
||||
}
|
||||
} else {
|
||||
labelValues = []string{strings.TrimSpace(view.LabelPattern)}
|
||||
}
|
||||
|
||||
filters = append(filters, &models.ReplicationFilter{
|
||||
Type: "label",
|
||||
Value: labelValues,
|
||||
Decoration: decoration,
|
||||
})
|
||||
}
|
||||
policy.Filters = filters
|
||||
|
||||
return policy
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package policies
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
)
|
||||
|
||||
func DeleteCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete [NAME|ID]",
|
||||
Short: "delete replication policy by name or id",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var rpolicyID int64
|
||||
if len(args) > 0 {
|
||||
var err error
|
||||
// convert string to int64
|
||||
rpolicyID, err = strconv.ParseInt(args[0], 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid replication policy ID: %s, %v", args[0], err)
|
||||
}
|
||||
} else {
|
||||
rpolicyID = prompt.GetReplicationPolicyFromUser()
|
||||
}
|
||||
|
||||
_, err := api.DeleteReplicationPolicy(rpolicyID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get replication policy: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
|
||||
fmt.Printf("Replication policy %d deleted successfully\n", rpolicyID)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package policies
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/replication/policies/list"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func ListCommand() *cobra.Command {
|
||||
var opts api.ListFlags
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List replication policies",
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
log.Debug("Starting replications list command")
|
||||
|
||||
if opts.PageSize > 100 {
|
||||
return fmt.Errorf("page size should be less than or equal to 100")
|
||||
}
|
||||
|
||||
log.Debug("Fetching projects...")
|
||||
allPolicies, err := api.ListReplicationPolicies(opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get projects list: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
|
||||
log.WithField("count", len(allPolicies.Payload)).Debug("Number of projects fetched")
|
||||
if len(allPolicies.Payload) == 0 {
|
||||
log.Info("No policies found")
|
||||
return nil
|
||||
}
|
||||
|
||||
formatFlag := viper.GetString("output-format")
|
||||
if formatFlag != "" {
|
||||
log.WithField("output_format", formatFlag).Debug("Output format selected")
|
||||
err = utils.PrintFormat(allPolicies.Payload, formatFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Debug("Listing projects using default view")
|
||||
list.ListPolicies(allPolicies.Payload)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.Int64VarP(&opts.Page, "page", "", 1, "Page number")
|
||||
flags.Int64VarP(&opts.PageSize, "page-size", "", 0, "Size of per page (0 to fetch all)")
|
||||
flags.StringVarP(&opts.Q, "query", "q", "", "Query string to query resources")
|
||||
flags.StringVarP(&opts.Sort, "sort", "", "", "Sort the resource list in ascending or descending order")
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package policies
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/replication/policies/create"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// UpdateCommand returns a command to update existing replication policies
|
||||
func UpdateCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "update [policy-id]",
|
||||
Short: "Update an existing replication policy",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var policyID int64
|
||||
if len(args) > 0 {
|
||||
var err error
|
||||
policyID, err = strconv.ParseInt(args[0], 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid replication policy ID: %s, %v", args[0], err)
|
||||
}
|
||||
} else {
|
||||
policyID = prompt.GetReplicationPolicyFromUser()
|
||||
}
|
||||
|
||||
existingPolicy, err := api.GetReplicationPolicy(policyID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get replication policy: %w", err)
|
||||
}
|
||||
|
||||
var existingReplicationMode string
|
||||
if existingPolicy.Payload.SrcRegistry.ID != 0 && existingPolicy.Payload.DestRegistry.ID == 0 {
|
||||
existingReplicationMode = "Pull"
|
||||
} else if existingPolicy.Payload.SrcRegistry.ID == 0 && existingPolicy.Payload.DestRegistry.ID != 0 {
|
||||
existingReplicationMode = "Push"
|
||||
} else {
|
||||
return fmt.Errorf("replication policy with ID %d is neither Pull nor Push", policyID)
|
||||
}
|
||||
|
||||
createView := &create.CreateView{
|
||||
Name: existingPolicy.Payload.Name,
|
||||
Description: existingPolicy.Payload.Description,
|
||||
Enabled: existingPolicy.Payload.Enabled,
|
||||
Override: existingPolicy.Payload.Override,
|
||||
ReplicateDeletion: existingPolicy.Payload.ReplicateDeletion,
|
||||
ReplicationMode: existingReplicationMode,
|
||||
}
|
||||
|
||||
if existingPolicy.Payload.CopyByChunk != nil {
|
||||
createView.CopyByChunk = *existingPolicy.Payload.CopyByChunk
|
||||
}
|
||||
|
||||
if existingPolicy.Payload.Speed != nil {
|
||||
if *existingPolicy.Payload.Speed == 0 {
|
||||
speed := int32(-1)
|
||||
existingPolicy.Payload.Speed = &speed
|
||||
}
|
||||
createView.Speed = strconv.FormatInt(int64(*existingPolicy.Payload.Speed), 10)
|
||||
}
|
||||
|
||||
if existingPolicy.Payload.SrcRegistry != nil && existingPolicy.Payload.DestRegistry == nil {
|
||||
createView.ReplicationMode = "Pull"
|
||||
} else if existingPolicy.Payload.SrcRegistry == nil && existingPolicy.Payload.DestRegistry != nil {
|
||||
createView.ReplicationMode = "Push"
|
||||
}
|
||||
|
||||
if existingPolicy.Payload.Trigger != nil {
|
||||
createView.TriggerType = existingPolicy.Payload.Trigger.Type
|
||||
|
||||
if existingPolicy.Payload.Trigger.TriggerSettings != nil {
|
||||
if existingPolicy.Payload.Trigger.Type == "scheduled" {
|
||||
createView.CronString = existingPolicy.Payload.Trigger.TriggerSettings.Cron
|
||||
} else if existingPolicy.Payload.Trigger.Type == "event_based" {
|
||||
createView.ReplicateDeletion = existingPolicy.Payload.ReplicateDeletion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("Updating replication policy: %s (ID: %d)", existingPolicy.Payload.Name, policyID)
|
||||
create.CreateRPolicyView(createView, true)
|
||||
|
||||
var updatedPolicy *models.ReplicationPolicy
|
||||
|
||||
fmt.Println("Updated policy replicate deletion:", createView.ReplicateDeletion)
|
||||
if createView.ReplicationMode == "Pull" {
|
||||
updatedPolicy = ConvertToPolicy(createView, existingPolicy.Payload.SrcRegistry)
|
||||
updatedPolicy.ID = policyID
|
||||
} else {
|
||||
updatedPolicy = ConvertToPolicy(createView, existingPolicy.Payload.DestRegistry)
|
||||
}
|
||||
|
||||
_, err = api.UpdateReplicationPolicy(policyID, updatedPolicy)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update replication policy: %w", err)
|
||||
}
|
||||
|
||||
log.Infof("Successfully updated replication policy: %s (ID: %d)", updatedPolicy.Name, policyID)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package policies
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
view "github.com/goharbor/harbor-cli/pkg/views/replication/policies/view"
|
||||
)
|
||||
|
||||
func ViewCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "view [NAME|ID]",
|
||||
Short: "get replication policy by name or id",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var rpolicyID int64
|
||||
if len(args) > 0 {
|
||||
var err error
|
||||
// convert string to int64
|
||||
rpolicyID, err = strconv.ParseInt(args[0], 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid replication policy ID: %s, %v", args[0], err)
|
||||
}
|
||||
} else {
|
||||
rpolicyID = prompt.GetReplicationPolicyFromUser()
|
||||
}
|
||||
|
||||
response, err := api.GetReplicationPolicy(rpolicyID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get replication policy: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
|
||||
FormatFlag := viper.GetString("output-format")
|
||||
if FormatFlag != "" {
|
||||
err = utils.PrintFormat(response.Payload, FormatFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
view.ViewPolicy(response.Payload)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package replication
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func StartCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "start",
|
||||
Short: "start replication",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
log.Debug("Starting replication")
|
||||
|
||||
var rpolicyID int64
|
||||
if len(args) > 0 {
|
||||
var err error
|
||||
// convert string to int64
|
||||
rpolicyID, err = strconv.ParseInt(args[0], 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid replication policy ID: %s, %v", args[0], err)
|
||||
}
|
||||
} else {
|
||||
rpolicyID = prompt.GetReplicationPolicyFromUser()
|
||||
}
|
||||
response, err := api.StartReplication(rpolicyID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start replication: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
fmt.Printf("Repliation started successfully with ID: %s\n", response.Location)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package replication
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func StopCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "stop",
|
||||
Short: "stop replication",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
log.Debug("Stopping replication")
|
||||
|
||||
var rpolicyID int64
|
||||
var executionID int64
|
||||
if len(args) > 0 {
|
||||
var err error
|
||||
// convert string to int64
|
||||
rpolicyID, err = strconv.ParseInt(args[0], 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid replication policy ID: %s, %v", args[0], err)
|
||||
}
|
||||
executionID = prompt.GetReplicationExecutionIDFromUser(rpolicyID)
|
||||
} else {
|
||||
rpolicyID = prompt.GetReplicationPolicyFromUser()
|
||||
executionID = prompt.GetReplicationExecutionIDFromUser(rpolicyID)
|
||||
}
|
||||
|
||||
execution, err := api.GetReplicationExecution(executionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get replication execution: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
if execution.Payload.Status != "InProgress" {
|
||||
return fmt.Errorf("replication execution with ID: %d is already stopped, succeed or failed", executionID)
|
||||
}
|
||||
|
||||
_, err = api.StopReplication(executionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stop replication: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
fmt.Printf("Replication execution with ID: %d stopped successfully\n", executionID)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -32,7 +32,11 @@ func RepoDeleteCmd() *cobra.Command {
|
|||
var projectName string
|
||||
var repoName string
|
||||
if len(args) > 0 {
|
||||
projectName, repoName = utils.ParseProjectRepo(args[0])
|
||||
projectName, repoName, err = utils.ParseProjectRepo(args[0])
|
||||
if err != nil {
|
||||
log.Errorf("failed to parse project/repo: %v", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
|
|
|
@ -52,7 +52,7 @@ func ListRepositoryCommand() *cobra.Command {
|
|||
}
|
||||
}
|
||||
|
||||
repos, err = api.ListRepository(projectName, false)
|
||||
repos, err = api.ListRepository(projectName, false, opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list repositories: %v", err)
|
||||
}
|
||||
|
|
|
@ -36,7 +36,11 @@ func RepoViewCmd() *cobra.Command {
|
|||
var repo *repository.GetRepositoryOK
|
||||
|
||||
if len(args) > 0 {
|
||||
projectName, repoName = utils.ParseProjectRepo(args[0])
|
||||
projectName, repoName, err = utils.ParseProjectRepo(args[0])
|
||||
if err != nil {
|
||||
log.Errorf("failed to parse project/repo: %v", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
projectName, err = prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package robot
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func Robot() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "robot",
|
||||
Short: "Manage robot accounts",
|
||||
Example: ` harbor robot list`,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
ListRobotCommand(),
|
||||
DeleteRobotCommand(),
|
||||
ViewRobotCommand(),
|
||||
CreateRobotCommand(),
|
||||
UpdateRobotCommand(),
|
||||
RefreshSecretCommand(),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,439 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package robot
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
config "github.com/goharbor/harbor-cli/pkg/config/robot"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/robot/create"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func CreateRobotCommand() *cobra.Command {
|
||||
var (
|
||||
opts create.CreateView
|
||||
all bool
|
||||
exportToFile bool
|
||||
configFile string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "create robot",
|
||||
Long: `Create a new robot account within Harbor.
|
||||
|
||||
Robot accounts are non-human users that can be used for automation purposes
|
||||
such as CI/CD pipelines, scripts, or other automated processes that need
|
||||
to interact with Harbor. They have specific permissions and a defined lifetime.
|
||||
|
||||
This command creates system-level robots that can have permissions spanning
|
||||
multiple projects, making them suitable for automation tasks that need access
|
||||
across your Harbor instance.
|
||||
|
||||
This command supports both interactive and non-interactive modes:
|
||||
- Without flags: opens an interactive form for configuring the robot
|
||||
- With flags: creates a robot with the specified parameters
|
||||
- With config file: loads robot configuration from YAML or JSON
|
||||
|
||||
A robot account requires:
|
||||
- A unique name
|
||||
- A set of system permissions
|
||||
- Optional project-specific permissions
|
||||
- A duration (lifetime in days)
|
||||
|
||||
The generated robot credentials can be:
|
||||
- Displayed on screen
|
||||
- Copied to clipboard (default)
|
||||
- Exported to a JSON file with the -e flag
|
||||
|
||||
Examples:
|
||||
# Interactive mode
|
||||
harbor-cli robot create
|
||||
|
||||
# Non-interactive mode with all flags
|
||||
harbor-cli robot create --name ci-robot --description "CI pipeline" --duration 90
|
||||
|
||||
# Create with all permissions
|
||||
harbor-cli robot create --name ci-robot --all-permission
|
||||
|
||||
# Load from configuration file
|
||||
harbor-cli robot create --robot-config-file ./robot-config.yaml
|
||||
|
||||
# Export secret to file
|
||||
harbor-cli robot create --name ci-robot --export-to-file`,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var permissions []models.Permission
|
||||
var projectPermissionsMap = make(map[string][]models.Permission)
|
||||
var accessesSystem []*models.Access
|
||||
|
||||
// Handle config file or interactive input
|
||||
if configFile != "" {
|
||||
if err := loadFromConfigFile(&opts, configFile, &permissions, projectPermissionsMap); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := handleInteractiveInput(&opts, all, &permissions, projectPermissionsMap); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Build system access permissions
|
||||
for _, perm := range permissions {
|
||||
accessesSystem = append(accessesSystem, &models.Access{
|
||||
Resource: perm.Resource,
|
||||
Action: perm.Action,
|
||||
})
|
||||
}
|
||||
|
||||
// Build merged permissions structure
|
||||
opts.Permissions = buildMergedPermissions(projectPermissionsMap, accessesSystem)
|
||||
opts.Level = "system"
|
||||
|
||||
// Create robot and handle response
|
||||
return createRobotAndHandleResponse(&opts, exportToFile)
|
||||
},
|
||||
}
|
||||
|
||||
addFlags(cmd, &opts, &all, &exportToFile, &configFile)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func loadFromConfigFile(opts *create.CreateView, configFile string, permissions *[]models.Permission, projectPermissionsMap map[string][]models.Permission) error {
|
||||
fmt.Println("Loading configuration from: ", configFile)
|
||||
|
||||
loadedOpts, err := config.LoadRobotConfigFromFile(configFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load robot config from file: %v", err)
|
||||
}
|
||||
|
||||
logrus.Info("Successfully loaded robot configuration")
|
||||
*opts = *loadedOpts
|
||||
|
||||
// Extract system-level and project permissions
|
||||
var systemPermFound bool
|
||||
for _, perm := range opts.Permissions {
|
||||
if perm.Kind == "system" && perm.Namespace == "/" {
|
||||
systemPermFound = true
|
||||
*permissions = make([]models.Permission, len(perm.Access))
|
||||
for i, access := range perm.Access {
|
||||
(*permissions)[i] = models.Permission{
|
||||
Resource: access.Resource,
|
||||
Action: access.Action,
|
||||
}
|
||||
}
|
||||
} else if perm.Kind == "project" {
|
||||
var projectPerms []models.Permission
|
||||
for _, access := range perm.Access {
|
||||
projectPerms = append(projectPerms, models.Permission{
|
||||
Resource: access.Resource,
|
||||
Action: access.Action,
|
||||
})
|
||||
}
|
||||
projectPermissionsMap[perm.Namespace] = projectPerms
|
||||
}
|
||||
}
|
||||
|
||||
if !systemPermFound {
|
||||
return fmt.Errorf("system robot configuration must include system-level permissions")
|
||||
}
|
||||
|
||||
logrus.Infof("Loaded system robot with %d system permissions and %d project-specific permissions",
|
||||
len(*permissions), len(projectPermissionsMap))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleInteractiveInput(opts *create.CreateView, all bool, permissions *[]models.Permission, projectPermissionsMap map[string][]models.Permission) error {
|
||||
// Show interactive form if needed
|
||||
if opts.Name == "" || opts.Duration == 0 {
|
||||
create.CreateRobotView(opts)
|
||||
}
|
||||
|
||||
// Validate duration
|
||||
if opts.Duration == 0 {
|
||||
return fmt.Errorf("failed to create robot: %v", utils.ParseHarborErrorMsg(fmt.Errorf("duration cannot be 0")))
|
||||
}
|
||||
|
||||
// Get system permissions
|
||||
if err := getSystemPermissions(all, permissions); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get project permissions
|
||||
return getProjectPermissions(opts, projectPermissionsMap)
|
||||
}
|
||||
|
||||
func getSystemPermissions(all bool, permissions *[]models.Permission) error {
|
||||
if len(*permissions) == 0 {
|
||||
if all {
|
||||
perms, _ := api.GetPermissions()
|
||||
for _, perm := range perms.Payload.System {
|
||||
*permissions = append(*permissions, *perm)
|
||||
}
|
||||
} else {
|
||||
*permissions = prompt.GetRobotPermissionsFromUser("system")
|
||||
if len(*permissions) == 0 {
|
||||
return fmt.Errorf("failed to create robot: %v",
|
||||
utils.ParseHarborErrorMsg(fmt.Errorf("no permissions selected, robot account needs at least one permission")))
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getProjectPermissions(opts *create.CreateView, projectPermissionsMap map[string][]models.Permission) error {
|
||||
permissionMode, err := promptPermissionMode()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error selecting permission mode: %v", err)
|
||||
}
|
||||
|
||||
switch permissionMode {
|
||||
case "list":
|
||||
return handleMultipleProjectsPermissions(projectPermissionsMap)
|
||||
case "per_project":
|
||||
return handlePerProjectPermissions(opts, projectPermissionsMap)
|
||||
case "none":
|
||||
fmt.Println("Creating robot with system-level permissions only (no project-specific permissions)")
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unknown permission mode: %s", permissionMode)
|
||||
}
|
||||
}
|
||||
|
||||
func handleMultipleProjectsPermissions(projectPermissionsMap map[string][]models.Permission) error {
|
||||
selectedProjects, err := getMultipleProjectsFromUser()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error selecting projects: %v", err)
|
||||
}
|
||||
|
||||
if len(selectedProjects) > 0 {
|
||||
fmt.Println("Select permissions to apply to all selected projects:")
|
||||
projectPermissions := prompt.GetRobotPermissionsFromUser("project")
|
||||
for _, projectName := range selectedProjects {
|
||||
projectPermissionsMap[projectName] = projectPermissions
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handlePerProjectPermissions(opts *create.CreateView, projectPermissionsMap map[string][]models.Permission) error {
|
||||
if opts.ProjectName == "" {
|
||||
for {
|
||||
projectName, err := prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
if projectName == "" {
|
||||
return fmt.Errorf("project name cannot be empty")
|
||||
}
|
||||
|
||||
projectPermissionsMap[projectName] = prompt.GetRobotPermissionsFromUser("project")
|
||||
|
||||
moreProjects, err := promptMoreProjects()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error asking for more projects: %v", err)
|
||||
}
|
||||
if !moreProjects {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
projectPermissions := prompt.GetRobotPermissionsFromUser("project")
|
||||
projectPermissionsMap[opts.ProjectName] = projectPermissions
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildMergedPermissions(projectPermissionsMap map[string][]models.Permission, accessesSystem []*models.Access) []*create.RobotPermission {
|
||||
var mergedPermissions []*create.RobotPermission
|
||||
|
||||
// Add project permissions
|
||||
for projectName, projectPermissions := range projectPermissionsMap {
|
||||
var accessesProject []*models.Access
|
||||
for _, perm := range projectPermissions {
|
||||
accessesProject = append(accessesProject, &models.Access{
|
||||
Resource: perm.Resource,
|
||||
Action: perm.Action,
|
||||
})
|
||||
}
|
||||
mergedPermissions = append(mergedPermissions, &create.RobotPermission{
|
||||
Namespace: projectName,
|
||||
Access: accessesProject,
|
||||
Kind: "project",
|
||||
})
|
||||
}
|
||||
|
||||
// Add system permissions
|
||||
mergedPermissions = append(mergedPermissions, &create.RobotPermission{
|
||||
Namespace: "/",
|
||||
Access: accessesSystem,
|
||||
Kind: "system",
|
||||
})
|
||||
|
||||
return mergedPermissions
|
||||
}
|
||||
|
||||
func createRobotAndHandleResponse(opts *create.CreateView, exportToFile bool) error {
|
||||
response, err := api.CreateRobot(*opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create robot: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
|
||||
logrus.Infof("Successfully created robot account '%s' (ID: %d)",
|
||||
response.Payload.Name, response.Payload.ID)
|
||||
|
||||
// Handle output format
|
||||
if formatFlag := viper.GetString("output-format"); formatFlag != "" {
|
||||
res, _ := api.GetRobot(response.Payload.ID)
|
||||
utils.SavePayloadJSON(response.Payload.Name, res.Payload)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handle secret output
|
||||
name, secret := response.Payload.Name, response.Payload.Secret
|
||||
|
||||
if exportToFile {
|
||||
logrus.Info("Exporting robot credentials to file")
|
||||
exportSecretToFile(name, secret, response.Payload.CreationTime.String(), response.Payload.ExpiresAt)
|
||||
return nil
|
||||
}
|
||||
|
||||
create.CreateRobotSecretView(name, secret)
|
||||
if err := clipboard.WriteAll(secret); err != nil {
|
||||
logrus.Errorf("failed to write to clipboard")
|
||||
} else {
|
||||
fmt.Println("secret copied to clipboard.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addFlags(cmd *cobra.Command, opts *create.CreateView, all *bool, exportToFile *bool, configFile *string) {
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(all, "all-permission", "a", false, "Select all permissions for the robot account")
|
||||
flags.BoolVarP(exportToFile, "export-to-file", "e", false, "Choose to export robot account to file")
|
||||
flags.StringVarP(&opts.ProjectName, "project", "", "", "set project name")
|
||||
flags.StringVarP(&opts.Name, "name", "", "", "name of the robot account")
|
||||
flags.StringVarP(&opts.Description, "description", "", "", "description of the robot account")
|
||||
flags.Int64VarP(&opts.Duration, "duration", "", 0, "set expiration of robot account in days")
|
||||
flags.StringVarP(configFile, "robot-config-file", "r", "", "YAML/JSON file with robot configuration")
|
||||
}
|
||||
|
||||
func exportSecretToFile(name, secret, creationTime string, expiresAt int64) {
|
||||
secretJson := config.RobotSecret{
|
||||
Name: name,
|
||||
ExpiresAt: expiresAt,
|
||||
CreationTime: creationTime,
|
||||
Secret: secret,
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("%s-secret.json", name)
|
||||
jsonData, err := json.MarshalIndent(secretJson, "", " ")
|
||||
if err != nil {
|
||||
logrus.Errorf("Failed to marshal secret to JSON: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filename, jsonData, 0600); err != nil {
|
||||
logrus.Errorf("Failed to write secret to file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Secret saved to %s\n", filename)
|
||||
}
|
||||
|
||||
func getMultipleProjectsFromUser() ([]string, error) {
|
||||
allProjects, err := api.ListAllProjects()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list projects: %v", err)
|
||||
}
|
||||
|
||||
var selectedProjects []string
|
||||
var projectOptions []huh.Option[string]
|
||||
|
||||
for _, p := range allProjects.Payload {
|
||||
projectOptions = append(projectOptions, huh.NewOption(p.Name, p.Name))
|
||||
}
|
||||
|
||||
err = huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewNote().
|
||||
Title("Multiple Project Selection").
|
||||
Description("Select the projects to assign the same permissions to this robot account."),
|
||||
huh.NewMultiSelect[string]().
|
||||
Title("Select projects").
|
||||
Options(projectOptions...).
|
||||
Value(&selectedProjects),
|
||||
),
|
||||
).WithTheme(huh.ThemeCharm()).WithWidth(80).Run()
|
||||
|
||||
return selectedProjects, err
|
||||
}
|
||||
|
||||
func promptMoreProjects() (bool, error) {
|
||||
var addMore bool
|
||||
err := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewNote().
|
||||
Title("Project Selection").
|
||||
Description("You can add permissions for multiple projects to this robot account."),
|
||||
huh.NewSelect[bool]().
|
||||
Title("Do you want to select (more) projects?").
|
||||
Description("Select 'Yes' to add (another) project, 'No' to continue with current selection.").
|
||||
Options(
|
||||
huh.NewOption("No", false),
|
||||
huh.NewOption("Yes", true),
|
||||
).
|
||||
Value(&addMore),
|
||||
),
|
||||
).WithTheme(huh.ThemeCharm()).WithWidth(60).WithHeight(10).Run()
|
||||
|
||||
return addMore, err
|
||||
}
|
||||
|
||||
func promptPermissionMode() (string, error) {
|
||||
var permissionMode string
|
||||
err := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewNote().
|
||||
Title("Permission Mode").
|
||||
Description("Select how you want to assign permissions to projects:"),
|
||||
huh.NewSelect[string]().
|
||||
Title("Permission Mode").
|
||||
Description("Choose 'List' to select multiple projects with common permissions, or 'Per Project' for individual project permissions.").
|
||||
Options(
|
||||
huh.NewOption("No project permissions (system-level only)", "none"),
|
||||
huh.NewOption("Per Project", "per_project"),
|
||||
huh.NewOption("List", "list"),
|
||||
).
|
||||
Value(&permissionMode),
|
||||
),
|
||||
).WithTheme(huh.ThemeCharm()).WithWidth(60).WithHeight(10).Run()
|
||||
|
||||
return permissionMode, err
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package robot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// to-do improve DeleteRobotCommand and multi select & delete
|
||||
func DeleteRobotCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete [robotID]",
|
||||
Short: "delete robot by id",
|
||||
Long: `Delete a robot account from Harbor.
|
||||
|
||||
This command permanently removes a robot account from Harbor. Once deleted,
|
||||
the robot's credentials will no longer be valid, and any automated processes
|
||||
using those credentials will fail.
|
||||
|
||||
The command supports multiple ways to identify the robot account to delete:
|
||||
- By providing the robot ID directly as an argument
|
||||
- Without any arguments, which will prompt for robot selection
|
||||
|
||||
Important considerations:
|
||||
- Deletion is permanent and cannot be undone
|
||||
- All access tokens for the robot will be invalidated immediately
|
||||
- Any systems using the robot's credentials will need to be updated
|
||||
- For system robots, access across all projects will be revoked
|
||||
|
||||
Examples:
|
||||
# Delete robot by ID
|
||||
harbor-cli robot delete 123
|
||||
|
||||
# Interactive deletion (will prompt for robot selection)
|
||||
harbor-cli robot delete`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var (
|
||||
robotID int64
|
||||
err error
|
||||
)
|
||||
if len(args) == 1 {
|
||||
robotID, err = strconv.ParseInt(args[0], 10, 64)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to parse robot ID: %v", err)
|
||||
}
|
||||
} else {
|
||||
robotID = prompt.GetRobotIDFromUser(-1)
|
||||
}
|
||||
err = api.DeleteRobot(robotID)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to delete robots: %v", utils.ParseHarborErrorMsg(err))
|
||||
return
|
||||
}
|
||||
log.Infof("Successfully deleted robot with ID: %d", robotID)
|
||||
fmt.Printf("Robot account (ID: %d) was successfully deleted\n", robotID)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package robot
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/robot/list"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// ListRobotCommand creates a new `harbor project robot list` command
|
||||
func ListRobotCommand() *cobra.Command {
|
||||
var opts api.ListFlags
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "list [projectName]",
|
||||
Short: "list robot",
|
||||
Long: `List robot accounts in Harbor.
|
||||
|
||||
This command displays a list of system-level robot accounts. The list includes basic
|
||||
information about each robot account, such as ID, name, creation time, and
|
||||
expiration status.
|
||||
|
||||
System-level robots have permissions that can span across multiple projects, making
|
||||
them suitable for CI/CD pipelines and automation tasks that require access to
|
||||
multiple projects in Harbor.
|
||||
|
||||
You can control the output using pagination flags and format options:
|
||||
- Use --page and --page-size to navigate through results
|
||||
- Use --sort to order the results by name, creation time, etc.
|
||||
- Use -q/--query to filter robots by specific criteria
|
||||
- Set output-format in your configuration for JSON, YAML, or other formats
|
||||
|
||||
Examples:
|
||||
# List all system robots
|
||||
harbor-cli robot list
|
||||
|
||||
# List system robots with pagination
|
||||
harbor-cli robot list --page 2 --page-size 20
|
||||
|
||||
# List system robots with custom sorting
|
||||
harbor-cli robot list --sort name
|
||||
|
||||
# Filter system robots by name
|
||||
harbor-cli robot list -q name=ci-robot
|
||||
|
||||
# Get robot details in JSON format
|
||||
harbor-cli robot list --output-format json`,
|
||||
Args: cobra.MaximumNArgs(0),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
robots, err := api.ListRobot(opts)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get robots list: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
|
||||
formatFlag := viper.GetString("output-format")
|
||||
if formatFlag != "" {
|
||||
err = utils.PrintFormat(robots, formatFlag)
|
||||
if err != nil {
|
||||
log.Errorf("Invalid Print Format: %v", err)
|
||||
}
|
||||
} else {
|
||||
list.ListRobots(robots.Payload)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.Int64VarP(&opts.Page, "page", "", 1, "Page number")
|
||||
flags.Int64VarP(&opts.PageSize, "page-size", "", 10, "Size of per page")
|
||||
flags.StringVarP(&opts.Q, "query", "q", "", "Query string to query resources")
|
||||
flags.StringVarP(
|
||||
&opts.Sort,
|
||||
"sort",
|
||||
"",
|
||||
"",
|
||||
"Sort the resource list in ascending or descending order",
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package robot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/robot/create"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func RefreshSecretCommand() *cobra.Command {
|
||||
var (
|
||||
robotID int64
|
||||
secret string
|
||||
secretStdin bool
|
||||
)
|
||||
cmd := &cobra.Command{
|
||||
Use: "refresh [robotID]",
|
||||
Short: "refresh robot secret by id",
|
||||
Long: `Refresh the secret for an existing robot account in Harbor.
|
||||
|
||||
This command generates a new secret for a robot account, effectively revoking
|
||||
the old secret and requiring updates to any systems using the robot's credentials.
|
||||
|
||||
The command supports multiple ways to identify the robot account:
|
||||
- By providing the robot ID directly as an argument
|
||||
- Without any arguments, which will prompt for both project and robot selection
|
||||
|
||||
You can specify the new secret in several ways:
|
||||
- Let Harbor generate a random secret (default)
|
||||
- Provide a custom secret with the --secret flag
|
||||
- Pipe a secret via stdin using the --secret-stdin flag
|
||||
|
||||
After refreshing, the new secret will be:
|
||||
- Displayed on screen
|
||||
- Copied to clipboard for immediate use
|
||||
- Usable immediately for authentication
|
||||
|
||||
Important considerations:
|
||||
- The old secret will be invalidated immediately
|
||||
- Any systems using the old credentials will need to be updated
|
||||
- There is no way to recover the old secret after refreshing
|
||||
|
||||
Examples:
|
||||
# Refresh robot secret by ID (generates a random secret)
|
||||
harbor-cli project robot refresh 123
|
||||
|
||||
# Refresh with a custom secret
|
||||
harbor-cli project robot refresh 123 --secret "MyCustomSecret123"
|
||||
|
||||
# Provide secret via stdin (useful for scripting)
|
||||
echo "MySecretFromScript123" | harbor-cli project robot refresh 123 --secret-stdin
|
||||
|
||||
# Interactive refresh (will prompt for project and robot selection)
|
||||
harbor-cli project robot refresh`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
if len(args) == 1 {
|
||||
robotID, err = strconv.ParseInt(args[0], 10, 64)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to parse robot ID: %v", err)
|
||||
}
|
||||
} else {
|
||||
robotID = prompt.GetRobotIDFromUser(-1)
|
||||
}
|
||||
|
||||
if secret != "" {
|
||||
err = utils.ValidatePassword(secret)
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid secret: %v\n", err)
|
||||
}
|
||||
}
|
||||
if secretStdin {
|
||||
secret = getSecret()
|
||||
}
|
||||
|
||||
response, err := api.RefreshSecret(secret, robotID)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to refresh robot secret: %v\n", err)
|
||||
}
|
||||
|
||||
log.Info("Secret updated successfully.")
|
||||
|
||||
if response.Payload.Secret != "" {
|
||||
secret = response.Payload.Secret
|
||||
create.CreateRobotSecretView("", secret)
|
||||
|
||||
err = clipboard.WriteAll(response.Payload.Secret)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to write the secret to the clipboard: %v", err)
|
||||
}
|
||||
fmt.Println("secret copied to clipboard.")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&secret, "secret", "", "", "secret")
|
||||
flags.BoolVarP(&secretStdin, "secret-stdin", "", false, "Take the robot secret from stdin")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// getSecret from commandline
|
||||
func getSecret() string {
|
||||
secret, err := utils.GetSecretStdin("Enter your secret: ")
|
||||
if err != nil {
|
||||
log.Fatalf("Error reading secret: %v\n", err)
|
||||
}
|
||||
|
||||
if err := utils.ValidatePassword(secret); err != nil {
|
||||
log.Fatalf("Invalid secret: %v\n", err)
|
||||
}
|
||||
return secret
|
||||
}
|
|
@ -0,0 +1,625 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package robot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
config "github.com/goharbor/harbor-cli/pkg/config/robot"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/robot/update"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func UpdateRobotCommand() *cobra.Command {
|
||||
var (
|
||||
robotID int64
|
||||
opts update.UpdateView
|
||||
all bool
|
||||
configFile string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "update [robotID]",
|
||||
Short: "update robot by id",
|
||||
Long: `Update an existing robot account within Harbor.
|
||||
|
||||
Robot accounts are non-human users that can be used for automation purposes
|
||||
such as CI/CD pipelines, scripts, or other automated processes that need
|
||||
to interact with Harbor. This command allows you to modify an existing robot's
|
||||
properties including its name, description, duration, and permissions.
|
||||
|
||||
This command supports both interactive and non-interactive modes:
|
||||
- With robot ID: directly updates the specified robot
|
||||
- Without ID: walks through robot selection interactively
|
||||
|
||||
The update process will:
|
||||
1. Identify the robot account to be updated
|
||||
2. Load its current configuration
|
||||
3. Apply the requested changes
|
||||
4. Save the updated configuration
|
||||
|
||||
This command can update both system and project-specific permissions:
|
||||
- System permissions apply across the entire Harbor instance
|
||||
- Project permissions apply to specific projects
|
||||
|
||||
Configuration can be loaded from:
|
||||
- Interactive prompts (default)
|
||||
- Command line flags
|
||||
- YAML/JSON configuration file
|
||||
|
||||
Note: Updating a robot does not regenerate its secret. If you need a new
|
||||
secret, consider deleting the robot and creating a new one instead.
|
||||
|
||||
Examples:
|
||||
# Update robot by ID with a new description
|
||||
harbor-cli robot update 123 --description "Updated CI/CD pipeline robot"
|
||||
|
||||
# Update robot's duration (extend lifetime)
|
||||
harbor-cli robot update 123 --duration 180
|
||||
|
||||
# Update with all permissions
|
||||
harbor-cli robot update 123 --all-permission
|
||||
|
||||
# Update from configuration file
|
||||
harbor-cli robot update 123 --robot-config-file ./robot-config.yaml
|
||||
|
||||
# Interactive update (will prompt for robot selection and changes)
|
||||
harbor-cli robot update`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
// Get robot ID from args or interactive prompt
|
||||
if len(args) == 1 {
|
||||
robotID, err = strconv.ParseInt(args[0], 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse robot ID: %v", err)
|
||||
}
|
||||
} else {
|
||||
robotID = prompt.GetRobotIDFromUser(-1)
|
||||
}
|
||||
|
||||
// Get current robot configuration
|
||||
robot, err := api.GetRobot(robotID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get robot: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
|
||||
// Initialize update view with current values
|
||||
bot := robot.Payload
|
||||
opts.ID = bot.ID
|
||||
opts.Level = bot.Level
|
||||
opts.Name = bot.Name
|
||||
opts.Secret = bot.Secret
|
||||
opts.Description = bot.Description
|
||||
opts.Duration = *bot.Duration
|
||||
opts.Disable = bot.Disable
|
||||
opts.Editable = bot.Editable
|
||||
opts.CreationTime = bot.CreationTime
|
||||
|
||||
// Extract current permissions (both system and project)
|
||||
var permissions []models.Permission
|
||||
var projectPermissionsMap = make(map[string][]models.Permission)
|
||||
|
||||
// Separate system and project permissions
|
||||
for _, perm := range bot.Permissions {
|
||||
if perm.Kind == "system" && perm.Namespace == "/" {
|
||||
for _, access := range perm.Access {
|
||||
permissions = append(permissions, models.Permission{
|
||||
Resource: access.Resource,
|
||||
Action: access.Action,
|
||||
})
|
||||
}
|
||||
} else if perm.Kind == "project" {
|
||||
var projectPerms []models.Permission
|
||||
for _, access := range perm.Access {
|
||||
projectPerms = append(projectPerms, models.Permission{
|
||||
Resource: access.Resource,
|
||||
Action: access.Action,
|
||||
})
|
||||
}
|
||||
projectPermissionsMap[perm.Namespace] = projectPerms
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Infof("Loaded robot with %d system permissions and %d project-specific permissions",
|
||||
len(permissions), len(projectPermissionsMap))
|
||||
|
||||
// Handle configuration from file or interactive input
|
||||
if configFile != "" {
|
||||
if err := loadFromConfigFileForUpdate(&opts, configFile, &permissions, projectPermissionsMap); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := handleInteractiveInputForUpdate(&opts, all, &permissions, projectPermissionsMap); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Build system access permissions
|
||||
var accessesSystem []*models.Access
|
||||
for _, perm := range permissions {
|
||||
accessesSystem = append(accessesSystem, &models.Access{
|
||||
Resource: perm.Resource,
|
||||
Action: perm.Action,
|
||||
})
|
||||
}
|
||||
|
||||
// Build merged permissions structure
|
||||
opts.Permissions = buildMergedPermissionsForUpdate(projectPermissionsMap, accessesSystem)
|
||||
|
||||
// Update robot and handle response
|
||||
return updateRobotAndHandleResponse(&opts)
|
||||
},
|
||||
}
|
||||
|
||||
addUpdateFlags(cmd, &opts, &all, &configFile)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func loadFromConfigFileForUpdate(opts *update.UpdateView, configFile string, permissions *[]models.Permission, projectPermissionsMap map[string][]models.Permission) error {
|
||||
fmt.Println("Loading configuration from: ", configFile)
|
||||
|
||||
loadedOpts, err := config.LoadRobotConfigFromFile(configFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load robot config from file: %v", err)
|
||||
}
|
||||
|
||||
logrus.Info("Successfully loaded robot configuration")
|
||||
|
||||
// Only update fields that should be updated from the config file
|
||||
// IMPORTANT: Do not update name or level as the Harbor API doesn't allow this
|
||||
// if loadedOpts.Name != "" {
|
||||
// opts.Name = loadedOpts.Name
|
||||
// }
|
||||
if loadedOpts.Description != "" {
|
||||
opts.Description = loadedOpts.Description
|
||||
}
|
||||
if loadedOpts.Duration != 0 {
|
||||
opts.Duration = loadedOpts.Duration
|
||||
}
|
||||
|
||||
var systemPermFound bool
|
||||
for _, perm := range loadedOpts.Permissions {
|
||||
if perm.Kind == "system" && perm.Namespace == "/" {
|
||||
systemPermFound = true
|
||||
for _, access := range perm.Access {
|
||||
*permissions = append(*permissions, models.Permission{
|
||||
Resource: access.Resource,
|
||||
Action: access.Action,
|
||||
})
|
||||
}
|
||||
} else if perm.Kind == "project" {
|
||||
var projectPerms []models.Permission
|
||||
for _, access := range perm.Access {
|
||||
projectPerms = append(projectPerms, models.Permission{
|
||||
Resource: access.Resource,
|
||||
Action: access.Action,
|
||||
})
|
||||
}
|
||||
// Validate project permissions before adding
|
||||
validProjectPerms, err := validateProjectPermissions(projectPerms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
projectPermissionsMap[perm.Namespace] = validProjectPerms
|
||||
}
|
||||
}
|
||||
|
||||
if !systemPermFound {
|
||||
return fmt.Errorf("robot configuration must include system-level permissions")
|
||||
}
|
||||
|
||||
logrus.Infof("Loaded robot update with %d system permissions and %d project-specific permissions",
|
||||
len(*permissions), len(projectPermissionsMap))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleInteractiveInputForUpdate(opts *update.UpdateView, all bool, permissions *[]models.Permission, projectPermissionsMap map[string][]models.Permission) error {
|
||||
// Show interactive form for updating basic details
|
||||
update.UpdateRobotView(opts)
|
||||
|
||||
// Validate duration
|
||||
if opts.Duration == 0 {
|
||||
return fmt.Errorf("failed to update robot: %v", utils.ParseHarborErrorMsg(fmt.Errorf("duration cannot be 0")))
|
||||
}
|
||||
|
||||
// Ask if user wants to update permissions
|
||||
var updatePerms bool
|
||||
err := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[bool]().
|
||||
Title("Do you want to update permissions?").
|
||||
Options(
|
||||
huh.NewOption("No", false),
|
||||
huh.NewOption("Yes", true),
|
||||
).
|
||||
Value(&updatePerms),
|
||||
),
|
||||
).WithTheme(huh.ThemeCharm()).WithWidth(60).Run()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error asking about permission updates: %v", err)
|
||||
}
|
||||
|
||||
if !updatePerms {
|
||||
logrus.Info("Keeping existing permissions")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get system permissions
|
||||
if err := getSystemPermissionsForUpdate(all, permissions); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get project permissions
|
||||
return getProjectPermissionsForUpdate(opts, projectPermissionsMap)
|
||||
}
|
||||
|
||||
func getSystemPermissionsForUpdate(all bool, permissions *[]models.Permission) error {
|
||||
var updateSystem bool
|
||||
err := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[bool]().
|
||||
Title("Do you want to update system permissions?").
|
||||
Options(
|
||||
huh.NewOption("No", false),
|
||||
huh.NewOption("Yes", true),
|
||||
).
|
||||
Value(&updateSystem),
|
||||
),
|
||||
).WithTheme(huh.ThemeCharm()).WithWidth(60).Run()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error asking about system permission updates: %v", err)
|
||||
}
|
||||
|
||||
if !updateSystem {
|
||||
logrus.Info("Keeping existing system permissions")
|
||||
return nil
|
||||
}
|
||||
|
||||
if all {
|
||||
perms, _ := api.GetPermissions()
|
||||
*permissions = nil // Clear existing permissions
|
||||
for _, perm := range perms.Payload.System {
|
||||
*permissions = append(*permissions, *perm)
|
||||
}
|
||||
} else {
|
||||
newPermissions := prompt.GetRobotPermissionsFromUser("system")
|
||||
if len(newPermissions) == 0 {
|
||||
return fmt.Errorf("failed to update robot: %v",
|
||||
utils.ParseHarborErrorMsg(fmt.Errorf("no permissions selected, robot account needs at least one permission")))
|
||||
}
|
||||
*permissions = newPermissions
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getProjectPermissionsForUpdate(opts *update.UpdateView, projectPermissionsMap map[string][]models.Permission) error {
|
||||
permissionMode, err := promptPermissionModeForUpdate(len(projectPermissionsMap) > 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error selecting permission mode: %v", err)
|
||||
}
|
||||
|
||||
switch permissionMode {
|
||||
case "keep":
|
||||
logrus.Info("Keeping existing project permissions")
|
||||
return nil
|
||||
case "clear":
|
||||
logrus.Info("Clearing all project permissions")
|
||||
// Clear the map to remove all project permissions
|
||||
for k := range projectPermissionsMap {
|
||||
delete(projectPermissionsMap, k)
|
||||
}
|
||||
return nil
|
||||
case "list":
|
||||
return handleMultipleProjectsPermissionsForUpdate(projectPermissionsMap)
|
||||
case "per_project":
|
||||
return handlePerProjectPermissionsForUpdate(projectPermissionsMap)
|
||||
default:
|
||||
return fmt.Errorf("unknown permission mode: %s", permissionMode)
|
||||
}
|
||||
}
|
||||
|
||||
func handleMultipleProjectsPermissionsForUpdate(projectPermissionsMap map[string][]models.Permission) error {
|
||||
// First, decide whether to replace or keep existing project permissions
|
||||
if len(projectPermissionsMap) > 0 {
|
||||
var replaceExisting bool
|
||||
err := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[bool]().
|
||||
Title("What do you want to do with existing project permissions?").
|
||||
Options(
|
||||
huh.NewOption("Keep existing and add new", false),
|
||||
huh.NewOption("Replace all existing with new selection", true),
|
||||
).
|
||||
Value(&replaceExisting),
|
||||
),
|
||||
).WithTheme(huh.ThemeCharm()).WithWidth(60).Run()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error asking about existing permissions: %v", err)
|
||||
}
|
||||
|
||||
if replaceExisting {
|
||||
// Clear the map to remove all project permissions
|
||||
for k := range projectPermissionsMap {
|
||||
delete(projectPermissionsMap, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectedProjects, err := getMultipleProjectsFromUser()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error selecting projects: %v", err)
|
||||
}
|
||||
|
||||
if len(selectedProjects) > 0 {
|
||||
fmt.Println("Select permissions to apply to all selected projects:")
|
||||
projectPermissions := prompt.GetRobotPermissionsFromUser("project")
|
||||
|
||||
// Validate project permissions
|
||||
validProjectPerms, err := validateProjectPermissions(projectPermissions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, projectName := range selectedProjects {
|
||||
projectPermissionsMap[projectName] = validProjectPerms
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handlePerProjectPermissionsForUpdate(projectPermissionsMap map[string][]models.Permission) error {
|
||||
// First, decide whether to replace or keep existing project permissions
|
||||
if len(projectPermissionsMap) > 0 {
|
||||
var modifyMode string
|
||||
err := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[string]().
|
||||
Title("How do you want to modify project permissions?").
|
||||
Options(
|
||||
huh.NewOption("Add new projects only", "add"),
|
||||
huh.NewOption("Modify existing projects", "modify"),
|
||||
huh.NewOption("Replace all existing with new projects", "replace"),
|
||||
).
|
||||
Value(&modifyMode),
|
||||
),
|
||||
).WithTheme(huh.ThemeCharm()).WithWidth(60).Run()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error asking about permission modification: %v", err)
|
||||
}
|
||||
|
||||
if modifyMode == "replace" {
|
||||
// Clear the map to remove all project permissions
|
||||
for k := range projectPermissionsMap {
|
||||
delete(projectPermissionsMap, k)
|
||||
}
|
||||
} else if modifyMode == "modify" {
|
||||
// Show existing projects and let user select which to modify
|
||||
var existingProjects []string
|
||||
for project := range projectPermissionsMap {
|
||||
existingProjects = append(existingProjects, project)
|
||||
}
|
||||
|
||||
var selectedProjects []string
|
||||
var projectOptions []huh.Option[string]
|
||||
|
||||
for _, p := range existingProjects {
|
||||
projectOptions = append(projectOptions, huh.NewOption(p, p))
|
||||
}
|
||||
|
||||
err = huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewMultiSelect[string]().
|
||||
Title("Select projects to modify").
|
||||
Options(projectOptions...).
|
||||
Value(&selectedProjects),
|
||||
),
|
||||
).WithTheme(huh.ThemeCharm()).WithWidth(80).Run()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error selecting projects to modify: %v", err)
|
||||
}
|
||||
|
||||
// Update permissions for selected projects
|
||||
for _, project := range selectedProjects {
|
||||
fmt.Printf("Updating permissions for project: %s\n", project)
|
||||
projectPerms := prompt.GetRobotPermissionsFromUser("project")
|
||||
|
||||
// Validate project permissions
|
||||
validProjectPerms, err := validateProjectPermissions(projectPerms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
projectPermissionsMap[project] = validProjectPerms
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Add new projects
|
||||
for {
|
||||
projectName, err := prompt.GetProjectNameFromUser()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
if projectName == "" {
|
||||
return fmt.Errorf("project name cannot be empty")
|
||||
}
|
||||
|
||||
projectPerms := prompt.GetRobotPermissionsFromUser("project")
|
||||
|
||||
// Validate project permissions
|
||||
validProjectPerms, err := validateProjectPermissions(projectPerms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
projectPermissionsMap[projectName] = validProjectPerms
|
||||
|
||||
moreProjects, err := promptMoreProjects()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error asking for more projects: %v", err)
|
||||
}
|
||||
if !moreProjects {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateProjectPermissions filters out permissions that are not valid for projects
|
||||
func validateProjectPermissions(permissions []models.Permission) ([]models.Permission, error) {
|
||||
perms, err := api.GetPermissions()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get valid permissions: %v", err)
|
||||
}
|
||||
|
||||
// Create a map of valid project permissions
|
||||
validProjectPerms := make(map[string]bool)
|
||||
for _, perm := range perms.Payload.Project {
|
||||
key := fmt.Sprintf("%s:%s", perm.Resource, perm.Action)
|
||||
validProjectPerms[key] = true
|
||||
}
|
||||
|
||||
// Filter the permissions
|
||||
var validPerms []models.Permission
|
||||
var invalidPerms []string
|
||||
|
||||
for _, perm := range permissions {
|
||||
key := fmt.Sprintf("%s:%s", perm.Resource, perm.Action)
|
||||
if validProjectPerms[key] {
|
||||
validPerms = append(validPerms, perm)
|
||||
} else {
|
||||
invalidPerms = append(invalidPerms, key)
|
||||
}
|
||||
}
|
||||
|
||||
// Warn about invalid permissions
|
||||
if len(invalidPerms) > 0 {
|
||||
logrus.Warnf("Removed %d invalid project permissions: %v", len(invalidPerms), invalidPerms)
|
||||
}
|
||||
|
||||
return validPerms, nil
|
||||
}
|
||||
|
||||
func buildMergedPermissionsForUpdate(projectPermissionsMap map[string][]models.Permission, accessesSystem []*models.Access) []*update.RobotPermission {
|
||||
var mergedPermissions []*update.RobotPermission
|
||||
|
||||
// Add project permissions
|
||||
for projectName, projectPermissions := range projectPermissionsMap {
|
||||
var accessesProject []*models.Access
|
||||
for _, perm := range projectPermissions {
|
||||
accessesProject = append(accessesProject, &models.Access{
|
||||
Resource: perm.Resource,
|
||||
Action: perm.Action,
|
||||
})
|
||||
}
|
||||
if len(accessesProject) > 0 {
|
||||
mergedPermissions = append(mergedPermissions, &update.RobotPermission{
|
||||
Namespace: projectName,
|
||||
Access: accessesProject,
|
||||
Kind: "project",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(accessesSystem) > 0 {
|
||||
// Add system permissions only if there are any
|
||||
mergedPermissions = append(mergedPermissions, &update.RobotPermission{
|
||||
Namespace: "/",
|
||||
Access: accessesSystem,
|
||||
Kind: "system",
|
||||
})
|
||||
}
|
||||
|
||||
return mergedPermissions
|
||||
}
|
||||
|
||||
func updateRobotAndHandleResponse(opts *update.UpdateView) error {
|
||||
err := api.UpdateRobot(opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update robot: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
|
||||
logrus.Infof("Successfully updated robot account '%s' (ID: %d)", opts.Name, opts.ID)
|
||||
|
||||
// Handle output format
|
||||
if formatFlag := viper.GetString("output-format"); formatFlag != "" {
|
||||
res, _ := api.GetRobot(opts.ID)
|
||||
utils.SavePayloadJSON(opts.Name, res.Payload)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addUpdateFlags(cmd *cobra.Command, opts *update.UpdateView, all *bool, configFile *string) {
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(all, "all-permission", "a", false, "Select all permissions for the robot account")
|
||||
flags.StringVarP(&opts.Name, "name", "", "", "name of the robot account")
|
||||
flags.StringVarP(&opts.Description, "description", "", "", "description of the robot account")
|
||||
flags.Int64VarP(&opts.Duration, "duration", "", 0, "set expiration of robot account in days")
|
||||
flags.StringVarP(configFile, "robot-config-file", "r", "", "YAML/JSON file with robot configuration")
|
||||
}
|
||||
|
||||
func promptPermissionModeForUpdate(hasExistingProjectPerms bool) (string, error) {
|
||||
var permissionMode string
|
||||
var options []huh.Option[string]
|
||||
|
||||
if hasExistingProjectPerms {
|
||||
options = []huh.Option[string]{
|
||||
huh.NewOption("Keep existing project permissions", "keep"),
|
||||
huh.NewOption("Clear all project permissions", "clear"),
|
||||
huh.NewOption("Per Project (individual permissions)", "per_project"),
|
||||
huh.NewOption("List (same permissions for multiple projects)", "list"),
|
||||
}
|
||||
} else {
|
||||
options = []huh.Option[string]{
|
||||
huh.NewOption("No project permissions (system-level only)", "clear"),
|
||||
huh.NewOption("Per Project (individual permissions)", "per_project"),
|
||||
huh.NewOption("List (same permissions for multiple projects)", "list"),
|
||||
}
|
||||
}
|
||||
|
||||
err := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewNote().
|
||||
Title("Project Permission Mode").
|
||||
Description("Select how you want to handle project permissions:"),
|
||||
huh.NewSelect[string]().
|
||||
Title("Permission Mode").
|
||||
Options(options...).
|
||||
Value(&permissionMode),
|
||||
),
|
||||
).WithTheme(huh.ThemeCharm()).WithWidth(60).WithHeight(10).Run()
|
||||
|
||||
return permissionMode, err
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package robot
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/goharbor/go-client/pkg/sdk/v2.0/client/robot"
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/prompt"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/robot/view"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func ViewRobotCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "view [robotID]",
|
||||
Short: "get robot by id",
|
||||
Long: `View detailed information about a robot account in Harbor.
|
||||
|
||||
This command displays comprehensive information about a robot account including
|
||||
its ID, name, description, creation time, expiration, and the permissions
|
||||
it has been granted. Supports both system-level and project-level robot accounts.
|
||||
|
||||
The command supports multiple ways to identify the robot account:
|
||||
- By providing the robot ID directly as an argument
|
||||
- Without any arguments, which will prompt for robot selection
|
||||
|
||||
The displayed information includes:
|
||||
- Basic details (ID, name, description)
|
||||
- Temporal information (creation date, expiration date, remaining time)
|
||||
- Security details (disabled status)
|
||||
- Detailed permissions breakdown by resource and action
|
||||
- For system robots: permissions across multiple projects are shown separately
|
||||
|
||||
System-level robots can have permissions spanning multiple projects, while
|
||||
project-level robots are scoped to a single project.
|
||||
|
||||
Examples:
|
||||
# View robot by ID
|
||||
harbor-cli robot view 123
|
||||
|
||||
# Interactive selection (will prompt for robot)
|
||||
harbor-cli robot view`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var (
|
||||
robot *robot.GetRobotByIDOK
|
||||
robotID int64
|
||||
err error
|
||||
)
|
||||
|
||||
if len(args) == 1 {
|
||||
robotID, err = strconv.ParseInt(args[0], 10, 64)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to parse robot ID: %v", err)
|
||||
}
|
||||
} else {
|
||||
robotID = prompt.GetRobotIDFromUser(-1)
|
||||
}
|
||||
|
||||
robot, err = api.GetRobot(robotID)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get robot: %v", err)
|
||||
}
|
||||
|
||||
// Convert to a list and display
|
||||
// robots := &models.Robot{robot.Payload}
|
||||
view.ViewRobot(robot.Payload)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package scan_all
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
func ScanAll() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "scan-all",
|
||||
Short: "Scan all artifacts",
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
UpdateScanAllScheduleCommand(),
|
||||
StopScanAllCommand(),
|
||||
ViewScanAllScheduleCommand(),
|
||||
GetScanAllMetricsCommand(),
|
||||
RunScanAllCommand(),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package scan_all
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
view "github.com/goharbor/harbor-cli/pkg/views/scan-all/metrics"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func GetScanAllMetricsCommand() *cobra.Command {
|
||||
var scheduled bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "metrics",
|
||||
Short: "Get the metrics of the latest scan all process",
|
||||
Long: `Display comprehensive metrics about the most recent vulnerability scan execution.
|
||||
|
||||
This command retrieves and displays detailed statistics about the most recent scan all
|
||||
process in Harbor, including:
|
||||
|
||||
- Running: Number of currently running scan tasks
|
||||
- Success: Number of successfully completed scan tasks
|
||||
- Error: Number of failed scan tasks
|
||||
- Completed: Total number of completed scan tasks
|
||||
- Total: Total number of scan tasks
|
||||
- Ongoing: Whether the scan is still in progress
|
||||
- Trigger: What triggered the scan (Manual, Scheduled, etc.)
|
||||
|
||||
The metrics provide visibility into the progress and results of vulnerability scanning across your Harbor registry.
|
||||
|
||||
Examples:
|
||||
# Get metrics for the latest scan
|
||||
harbor-cli scan-all metrics
|
||||
|
||||
# Get metrics for the latest scheduled scan
|
||||
harbor-cli scan-all metrics --scheduled
|
||||
|
||||
# Display metrics in JSON format
|
||||
harbor-cli scan-all metrics --output-format json`,
|
||||
Args: cobra.MaximumNArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
logrus.Info("Retrieving scan all metrics")
|
||||
metrics, err := api.GetScanAllMetrics(scheduled)
|
||||
if err != nil {
|
||||
logrus.Errorf("Failed to retrieve scan all metrics: %v", utils.ParseHarborErrorMsg(err))
|
||||
return err
|
||||
}
|
||||
|
||||
FormatFlag := viper.GetString("output-format")
|
||||
if FormatFlag != "" {
|
||||
err = utils.PrintFormat(metrics, FormatFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
view.ViewScanMetrics(metrics)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
// latest scheduled metrics is deprecated in the API
|
||||
flags.BoolVarP(&scheduled, "scheduled", "s", false, "Get the metrics of the latest scheduled scan all process")
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package scan_all
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func RunScanAllCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "run",
|
||||
Short: "Scan all artifacts now",
|
||||
Long: `Initiate an immediate vulnerability scan of all artifacts in Harbor.
|
||||
|
||||
This command triggers a manual scan of all artifacts across all projects in your Harbor instance.
|
||||
The scan will check for known vulnerabilities in container images using the configured scanner(s).
|
||||
|
||||
The scan runs asynchronously in the background. After initiating the scan, you can:
|
||||
- Check the scan progress with 'harbor-cli scan-all metrics'
|
||||
- View results through the Harbor UI
|
||||
|
||||
Important considerations:
|
||||
- This operation can be resource intensive on large registries
|
||||
- Scanning many artifacts simultaneously may impact system performance
|
||||
- The time to complete depends on the number and size of artifacts
|
||||
- Only one scan-all operation can run at a time
|
||||
|
||||
Examples:
|
||||
# Start scanning all artifacts immediately
|
||||
harbor-cli scan-all run
|
||||
|
||||
# Start scanning and monitor progress
|
||||
harbor-cli scan-all run && watch -n 0.2 harbor-cli scan-all metrics
|
||||
|
||||
The scan progress and results can be monitored through the metrics command
|
||||
or through the Harbor web interface.`,
|
||||
Args: cobra.MaximumNArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
logrus.Info("Initiating manual scan of all artifacts")
|
||||
// Random cron expression and random time need to be passed to the API, even though they are not used, otherwise it returns bad request
|
||||
randomCron := "0 * * * * *"
|
||||
randomTime := strfmt.DateTime{}
|
||||
err := api.CreateScanAllSchedule(models.ScheduleObj{Type: "Manual", Cron: randomCron, NextScheduledTime: randomTime})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start scan all operation: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
logrus.Info("Successfully started scan all operation")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package scan_all
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func StopScanAllCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "stop",
|
||||
Short: "Stop scanning all artifacts",
|
||||
Long: `Stop an ongoing vulnerability scan of all artifacts in Harbor.
|
||||
|
||||
This command halts the current scan-all operation that was either manually triggered
|
||||
or scheduled. When stopped, scans that are already in progress will complete, but no new artifacts will be scanned. The scan can be restarted later using the 'scan-all run' command.
|
||||
|
||||
Examples:
|
||||
# Stop the current scan-all operation
|
||||
harbor-cli scan-all stop
|
||||
|
||||
# Stop and then check metrics to confirm
|
||||
harbor-cli scan-all stop && harbor-cli scan-all metrics`,
|
||||
Args: cobra.MaximumNArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
logrus.Info("Stopping scan all operation")
|
||||
err := api.StopScanAll()
|
||||
if err != nil {
|
||||
logrus.Errorf("Failed to stop scan all operation: %v", utils.ParseHarborErrorMsg(err))
|
||||
return err
|
||||
}
|
||||
logrus.Info("Successfully stopped scan all operation")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package scan_all
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/scan-all/update"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
var validScheduleTypes = map[string]bool{
|
||||
"None": true,
|
||||
"Hourly": true,
|
||||
"Daily": true,
|
||||
"Weekly": true,
|
||||
"Custom": true,
|
||||
}
|
||||
|
||||
func UpdateScanAllScheduleCommand() *cobra.Command {
|
||||
var scheduleType string
|
||||
var cron string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "update-schedule",
|
||||
Short: "update-schedule [schedule-type: none|hourly|daily|weekly|custom]",
|
||||
Long: `Configure or update the automatic vulnerability scan schedule for all artifacts.
|
||||
|
||||
This command allows you to set when Harbor automatically scans all artifacts for vulnerabilities. You can choose from predefined schedules or create a custom schedule using cron expressions.
|
||||
|
||||
Available schedule types:
|
||||
- none: Disable automatic scanning
|
||||
- hourly: Run scan every hour
|
||||
- daily: Run scan once per day
|
||||
- weekly: Run scan once per week
|
||||
- custom: Define a custom schedule using a cron expression
|
||||
|
||||
For custom schedules, Harbor requires a 6-field cron expression in the format:
|
||||
seconds minutes hours day-of-month month day-of-week
|
||||
|
||||
Examples:
|
||||
# Disable scheduled scanning
|
||||
harbor-cli scan-all update-schedule none
|
||||
|
||||
# Set daily automatic scanning
|
||||
harbor-cli scan-all update-schedule daily
|
||||
|
||||
# Set weekly automatic scanning
|
||||
harbor-cli scan-all update-schedule weekly
|
||||
|
||||
# Set a custom schedule (every day at 2:30 AM)
|
||||
harbor-cli scan-all update-schedule custom --cron "0 30 2 * * *"
|
||||
|
||||
# Use interactive mode to configure a custom schedule
|
||||
harbor-cli scan-all update-schedule custom
|
||||
|
||||
Note: For custom schedules, if you provide a 5-field cron expression, the CLI will automatically add a leading "0" for the seconds field to create the required 6-field format.`,
|
||||
Aliases: []string{"us"},
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
scheduleType = cases.Title(language.English).String(strings.ToLower(args[0]))
|
||||
|
||||
if !validScheduleTypes[scheduleType] {
|
||||
return fmt.Errorf("invalid schedule type: %s. Valid types are: none, hourly, daily, weekly, custom", args[0])
|
||||
}
|
||||
|
||||
logrus.Infof("Updating scan all schedule to type: %s", scheduleType)
|
||||
|
||||
switch scheduleType {
|
||||
case "None":
|
||||
return updateScheduleToNone()
|
||||
|
||||
case "Hourly", "Daily", "Weekly":
|
||||
return updatePredefinedSchedule(scheduleType)
|
||||
|
||||
case "Custom":
|
||||
return updateCustomSchedule(cron)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVar(&cron, "cron", "", "Cron expression for custom schedule (include the expression in double quotes)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func updateScheduleToNone() error {
|
||||
logrus.Info("Setting scan all schedule to None (disabled)")
|
||||
err := api.UpdateScanAllSchedule(models.ScheduleObj{Type: "None"})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to disable scan schedule: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
logrus.Info("Successfully disabled scan all schedule")
|
||||
return nil
|
||||
}
|
||||
|
||||
func updatePredefinedSchedule(scheduleType string) error {
|
||||
logrus.Infof("Setting scan all schedule to %s", scheduleType)
|
||||
|
||||
// Random cron expression and time needed by API
|
||||
randomCron := "0 0 * * * * "
|
||||
randomTime := strfmt.DateTime{}
|
||||
|
||||
err := api.UpdateScanAllSchedule(models.ScheduleObj{
|
||||
Type: scheduleType,
|
||||
Cron: randomCron,
|
||||
NextScheduledTime: randomTime,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update scan schedule: %v", utils.ParseHarborErrorMsg(err))
|
||||
}
|
||||
|
||||
logrus.Infof("Successfully set scan all schedule to %s", scheduleType)
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateCustomSchedule(cron string) error {
|
||||
if cron == "" {
|
||||
logrus.Info("Opening interactive form for custom schedule configuration")
|
||||
update.UpdateSchedule(&cron)
|
||||
}
|
||||
|
||||
if err := validateCron(cron); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("Setting scan all schedule with custom cron expression: %s", cron)
|
||||
|
||||
// Random time needed by API
|
||||
randomTime := strfmt.DateTime{}
|
||||
err := api.UpdateScanAllSchedule(models.ScheduleObj{
|
||||
Type: "Custom",
|
||||
Cron: cron,
|
||||
NextScheduledTime: randomTime,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
errMsg := utils.ParseHarborErrorMsg(err)
|
||||
if strings.Contains(errMsg, "400") {
|
||||
return fmt.Errorf("invalid cron expression: Harbor rejected the schedule. Use the standard 5-field format (minute hour day month weekday)")
|
||||
}
|
||||
return fmt.Errorf("failed to update scan schedule: %v", errMsg)
|
||||
}
|
||||
|
||||
logrus.Info("Successfully set scan all schedule with custom cron expression")
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCron(cron string) error {
|
||||
if cron == "" {
|
||||
return errors.New("cron expression cannot be empty")
|
||||
}
|
||||
fields := strings.Fields(cron)
|
||||
if len(fields) < 6 {
|
||||
if len(fields) == 5 {
|
||||
logrus.Infof("Converting 5-field cron to 6-field by adding '0' for seconds")
|
||||
return fmt.Errorf("harbor requires 6-field cron format (including seconds). Try: '0 %s'", cron)
|
||||
}
|
||||
return fmt.Errorf("harbor requires 6-field cron format (seconds minute hour day month weekday)")
|
||||
}
|
||||
if len(fields) > 6 {
|
||||
return fmt.Errorf("too many fields in cron expression, expected 6 but got %d", len(fields))
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package scan_all
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor-cli/pkg/api"
|
||||
"github.com/goharbor/harbor-cli/pkg/utils"
|
||||
"github.com/goharbor/harbor-cli/pkg/views/scan-all/view-schedule"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// This command does not work because the API does not return the response body
|
||||
// API: https://demo.goharbor.io/devcenter-api-2.0
|
||||
func ViewScanAllScheduleCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "view-schedule",
|
||||
Short: "View the scan all schedule",
|
||||
Long: `Display the current vulnerability scan schedule configuration.
|
||||
|
||||
This command retrieves and shows the current automatic scanning schedule settings for your Harbor instance, including:
|
||||
|
||||
- Schedule Type: The type of schedule (None, Hourly, Daily, Weekly, or Custom)
|
||||
- Cron Expression: For custom schedules, shows the configured cron pattern
|
||||
- Next Scheduled Time: When the next automatic scan is scheduled to run
|
||||
|
||||
This information helps you understand when Harbor will automatically scan your artifacts
|
||||
for vulnerabilities.
|
||||
|
||||
Examples:
|
||||
# View the current scan schedule
|
||||
harbor-cli scan-all view-schedule
|
||||
|
||||
# View the schedule in JSON format
|
||||
harbor-cli scan-all view-schedule --output-format json
|
||||
|
||||
You can use this command to verify changes after updating the schedule with the 'update-schedule' command.`,
|
||||
Args: cobra.MaximumNArgs(0),
|
||||
Aliases: []string{"vs"},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
logrus.Info("Retrieving scan all schedule configuration")
|
||||
schedule, err := api.GetScanAllSchedule()
|
||||
if err != nil {
|
||||
logrus.Errorf("Failed to retrieve scan all schedule: %v", utils.ParseHarborErrorMsg(err))
|
||||
return err
|
||||
}
|
||||
|
||||
FormatFlag := viper.GetString("output-format")
|
||||
if FormatFlag != "" {
|
||||
err = utils.PrintFormat(schedule, FormatFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
view.ViewScanSchedule(schedule)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -36,6 +36,10 @@ func ElevateUserCmd() *cobra.Command {
|
|||
log.Errorf("failed to get user id for '%s': %v", args[0], err)
|
||||
return
|
||||
}
|
||||
if userId == 0 {
|
||||
log.Errorf("User with name '%s' not found", args[0])
|
||||
return
|
||||
}
|
||||
} else {
|
||||
userId = prompt.GetUserIdFromUser()
|
||||
}
|
||||
|
@ -50,11 +54,13 @@ func ElevateUserCmd() *cobra.Command {
|
|||
}
|
||||
|
||||
err = api.ElevateUser(userId)
|
||||
if err != nil {
|
||||
if isUnauthorizedError(err) {
|
||||
log.Error("Permission denied: Admin privileges are required to execute this command.")
|
||||
} else {
|
||||
log.Errorf("failed to elevate user: %v", err)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "harbor-cli",
|
||||
"engineVersion": "v0.18.4",
|
||||
"engineVersion": "v0.18.10",
|
||||
"sdk": {
|
||||
"source": "go"
|
||||
},
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
title: Harbor CLI Documentation
|
||||
---
|
||||
|
||||
Welcome to the Harbor CLI documentation. This provides detailed documentation for the Harbor CLI.
|
||||
|
||||
## Harbor CLI Documentation
|
||||
|
||||
This section describes the comprehensive set of commands provided by the Harbor CLI, which enables you to efficiently manage and interact with your Harbor registry.
|
||||
|
||||
- `harbor` - Configure the Harbor CLI and set global flags to customize your experience.
|
||||
- `harbor artifact` - Manage artifacts in Harbor Repository
|
||||
- `harbor project` - Manage projects and assign resources to them
|
||||
- `harbor registry` - Manage registries in Harbor
|
||||
- `harbor repo` - Manage repositories in Harbor context
|
||||
- `harbor user` - Administer users in Harbor, including creating, updating, and managing user accounts
|
||||
|
||||
## Access the Documentation Source Files
|
||||
|
||||
The source files for this documentation set are located in the [Harbor CLI repository on Github](https://github.com/goharbor/harbor-cli/tree/main/doc/cli-docs).
|
|
@ -0,0 +1,110 @@
|
|||
---
|
||||
title: Harbor CLI Config Management
|
||||
weight: 25
|
||||
---
|
||||
|
||||
# Harbor CLI Configuration Management
|
||||
|
||||
> **Note**
|
||||
> The Harbor CLI follows the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) for configuration and data storage by default.
|
||||
|
||||
## Introduction
|
||||
Harbor CLI is a flexible command-line tool that lets you manage various Harbor environments with different credentials. Whether you need a production-ready setup or quick testing configurations, the CLI's hierarchical structure and XDG support help keep things organized.
|
||||
|
||||
|
||||
## Understanding the Configuration Structure
|
||||
The Harbor CLI can manage multiple credentials and keep track of which credential is currently active. This setup allows you to maintain separate contexts for different Harbor instances or user accounts without having to rewrite configuration files manually. While the Harbor CLI configuration file manages your credentials, passwords themselves are never stored in plain text. Instead, they are secured using the AES-GCM encryption described in the [Harbor CLI Encryption documentation](../cli-config).
|
||||
|
||||
### Example Configuration File
|
||||
Below is a simplified example of a typical Harbor CLI configuration file:
|
||||
```yaml
|
||||
current-credential-name: example@demo-harbor
|
||||
credentials:
|
||||
- name: example@demo-harbor
|
||||
username: example-user
|
||||
password: example-password
|
||||
serveraddress: https://demo.goharbor.io
|
||||
```
|
||||
|
||||
In this configuration:
|
||||
- **current-credential-name** references the active credential by name.
|
||||
- **credentials** holds one or more sets of user credentials, each following the same structure.
|
||||
|
||||
## Managing Multiple Credentials
|
||||
If you need to work with multiple sets of credentials—such as development, staging, or production — Harbor CLI makes it easy to create and switch between them.
|
||||
|
||||
> **Note**: For more login command details please refer to the [login command reference](../cli-docs/harbor-login.md).
|
||||
### Creating a New Credentials Entry
|
||||
Use the `harbor login` command with the required arguments to store new credentials:
|
||||
```bash
|
||||
harbor login --name my-new-credential \
|
||||
--username myuser \
|
||||
--password mypass \
|
||||
https://my-harbor-instance.com
|
||||
```
|
||||
This adds a new entry to your credentials list, allowing you to manage different Harbor accounts from the same CLI.
|
||||
|
||||
### Switching Between Credentials
|
||||
To switch to another credential set, run:
|
||||
```bash
|
||||
harbor login --name <name-of-credential>
|
||||
```
|
||||
The CLI will then set the specified credential as the active one, eliminating the need to manually edit your configuration files. This will overwrite the `current-credential-name`.
|
||||
|
||||
|
||||
## Configuration Hierarchy (Highest to Lowest Priority)
|
||||
|
||||
1. **Explicit Config Flag**
|
||||
Provide a custom config file at runtime using `--config`:
|
||||
```bash
|
||||
harbor --config /path/to/custom/config.yaml artifact list
|
||||
```
|
||||
|
||||
2. **Environment Variable**
|
||||
Set a persistent configuration through the `HARBOR_CLI_CONFIG` environment variable:
|
||||
```bash
|
||||
export HARBOR_CLI_CONFIG="$HOME/.custom/harbor-config.yaml"
|
||||
harbor artifact list # Uses the environment-specified config
|
||||
```
|
||||
|
||||
3. **XDG Default Paths**
|
||||
Automatically discover configuration in the following order:
|
||||
```bash
|
||||
${XDG_CONFIG_HOME}/harbor-cli/config.yaml # If XDG_CONFIG_HOME is set
|
||||
~/.config/harbor-cli/config.yaml # Fallback default
|
||||
```
|
||||
|
||||
## Data Storage Management
|
||||
### Data File Location
|
||||
|
||||
- **Primary Path**: `$XDG_DATA_HOME/harbor-cli/data.yaml`
|
||||
- **Fallback Path**: `$HOME/.local/share/harbor-cli/data.yaml`
|
||||
|
||||
> **Important**
|
||||
> The data file automatically tracks the last-used configuration file path
|
||||
|
||||
## Configuration Precedence Summary
|
||||
| Priority | Method | Example |
|
||||
|----------|----------------------------|---------------------------------------|
|
||||
| 1 | --config flag | harbor --config ./test.yaml ... |
|
||||
| 2 | HARBOR_CLI_CONFIG env var | export HARBOR_CLI_CONFIG=... |
|
||||
| 3 | XDG Default Locations | ~/.config/harbor-cli/config.yaml |
|
||||
|
||||
## Practical Usage Examples
|
||||
### Scenario 1: Temporary Config Override
|
||||
```bash
|
||||
harbor --config ./experimental.yaml project create "new-project"
|
||||
```
|
||||
|
||||
### Scenario 2: Persistent Environment-based Config
|
||||
```bash
|
||||
echo 'export HARBOR_CLI_CONFIG="$HOME/work/configs/prod-harbor.yaml"' >> ~/.zshrc
|
||||
source ~/.zshrc
|
||||
harbor config list # Uses production config
|
||||
```
|
||||
|
||||
### Scenario 3: Reset to Default Configuration
|
||||
```bash
|
||||
unset HARBOR_CLI_CONFIG
|
||||
harbor config delete --current # Deletes current context
|
||||
```
|
|
@ -0,0 +1,48 @@
|
|||
---
|
||||
title: harbor artifact label add
|
||||
weight: 95
|
||||
---
|
||||
## harbor artifact label add
|
||||
|
||||
### Description
|
||||
|
||||
##### Attach a label to an artifact in a Harbor project repository
|
||||
|
||||
### Synopsis
|
||||
|
||||
Attach an existing label to a specific artifact identified by <project>/<repository>:<reference>.
|
||||
You can specify the artifact and label directly as arguments, or interactively select them if arguments are omitted.
|
||||
|
||||
Examples:
|
||||
# Add a label to an artifact using project/repo:reference and label name
|
||||
harbor artifact label add myproject/myrepo@sha256:abcdef1234567890 dev
|
||||
|
||||
# Prompt-based label selection for an artifact
|
||||
harbor artifact label add library/nginx:1.21
|
||||
|
||||
# Fully interactive mode (prompt for everything)
|
||||
harbor artifact label add
|
||||
|
||||
|
||||
```sh
|
||||
harbor artifact label add [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```sh
|
||||
-h, --help help for add
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```sh
|
||||
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
|
||||
-o, --output-format string Output format. One of: json|yaml
|
||||
-v, --verbose verbose output
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [harbor artifact label](harbor-artifact-label.md) - label command for artifacts
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
---
|
||||
title: harbor artifact label delete
|
||||
weight: 90
|
||||
---
|
||||
## harbor artifact label delete
|
||||
|
||||
### Description
|
||||
|
||||
##### Detach a label from an artifact in a Harbor project repository
|
||||
|
||||
### Synopsis
|
||||
|
||||
Remove an existing label from a specific artifact identified by <project>/<repository>:<reference>.
|
||||
You can provide the artifact and label name as arguments, or choose them interactively if not specified.
|
||||
|
||||
Examples:
|
||||
# Remove a label by specifying artifact and label name
|
||||
harbor artifact label delete library/nginx:latest stable
|
||||
|
||||
# Prompt-based label selection for a specific artifact
|
||||
harbor artifact label del library/nginx:1.21
|
||||
|
||||
# Fully interactive mode (prompt for project, repo, reference, and label)
|
||||
harbor artifact label delete
|
||||
|
||||
# Remove a label from an artifact identified by digest
|
||||
harbor artifact label del myproject/myrepo@sha256:abcdef1234567890 qa-label
|
||||
|
||||
```sh
|
||||
harbor artifact label delete [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```sh
|
||||
-h, --help help for delete
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```sh
|
||||
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
|
||||
-o, --output-format string Output format. One of: json|yaml
|
||||
-v, --verbose verbose output
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [harbor artifact label](harbor-artifact-label.md) - label command for artifacts
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
title: harbor artifact label list
|
||||
weight: 30
|
||||
---
|
||||
## harbor artifact label list
|
||||
|
||||
### Description
|
||||
|
||||
##### Display labels attached to a specific artifact
|
||||
|
||||
### Synopsis
|
||||
|
||||
This command lists all labels currently associated with a specific artifact in a Harbor project repository.
|
||||
You can provide the artifact reference in the format <project>/<repository>:<reference> (where reference is either a tag or a digest).
|
||||
If the reference is not provided as an argument, the command will prompt you to select the project, repository, and artifact.
|
||||
|
||||
Supports output formatting such as JSON or YAML using the --output (-o) flag.
|
||||
|
||||
```sh
|
||||
harbor artifact label list [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```sh
|
||||
# List labels for a tagged artifact
|
||||
harbor artifact label list library/nginx:latest
|
||||
|
||||
# List labels for an artifact by digest
|
||||
harbor artifact label list myproject/myrepo@sha256:abc123...
|
||||
|
||||
# Prompt-based interactive selection of artifact
|
||||
harbor artifact label list
|
||||
|
||||
# Output in JSON format
|
||||
harbor artifact label list library/nginx:1.21 -o json
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```sh
|
||||
-h, --help help for list
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```sh
|
||||
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
|
||||
-o, --output-format string Output format. One of: json|yaml
|
||||
-v, --verbose verbose output
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [harbor artifact label](harbor-artifact-label.md) - label command for artifacts
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
title: harbor artifact label
|
||||
weight: 70
|
||||
---
|
||||
## harbor artifact label
|
||||
|
||||
### Description
|
||||
|
||||
##### label command for artifacts
|
||||
|
||||
### Synopsis
|
||||
|
||||
label command for artifact
|
||||
|
||||
### Examples
|
||||
|
||||
```sh
|
||||
harbor artifact label add <project>/<repository>/<reference> <label name>
|
||||
harbor artifact label del <project>/<repository>/<reference> <label name>
|
||||
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```sh
|
||||
-h, --help help for label
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```sh
|
||||
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
|
||||
-o, --output-format string Output format. One of: json|yaml
|
||||
-v, --verbose verbose output
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [harbor artifact](harbor-artifact.md) - Manage artifacts
|
||||
* [harbor artifact label add](harbor-artifact-label-add.md) - Attach a label to an artifact in a Harbor project repository
|
||||
* [harbor artifact label delete](harbor-artifact-label-delete.md) - Detach a label from an artifact in a Harbor project repository
|
||||
* [harbor artifact label list](harbor-artifact-label-list.md) - Display labels attached to a specific artifact
|
||||
|
|
@ -36,6 +36,7 @@ Manage artifacts in Harbor Repository
|
|||
|
||||
* [harbor](harbor.md) - Official Harbor CLI
|
||||
* [harbor artifact delete](harbor-artifact-delete.md) - delete an artifact
|
||||
* [harbor artifact label](harbor-artifact-label.md) - label command for artifacts
|
||||
* [harbor artifact list](harbor-artifact-list.md) - List container artifacts (images, charts, etc.) in a Harbor repository with metadata
|
||||
* [harbor artifact scan](harbor-artifact-scan.md) - Scan an artifact
|
||||
* [harbor artifact tags](harbor-artifact-tags.md) - Manage tags of an artifact
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
---
|
||||
title: harbor config
|
||||
weight: 30
|
||||
---
|
||||
## harbor config
|
||||
|
||||
### Description
|
||||
|
||||
##### Manage the config of the Harbor Cli
|
||||
|
||||
### Synopsis
|
||||
|
||||
Manage repositories in Harbor config
|
||||
|
||||
### Options
|
||||
|
||||
```sh
|
||||
-h, --help help for config
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```sh
|
||||
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
|
||||
-o, --output-format string Output format. One of: json|yaml
|
||||
-v, --verbose verbose output
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [harbor](harbor.md) - Official Harbor CLI
|
||||
* [harbor config delete](harbor-config-delete.md) - Delete (clear) a specific config item
|
||||
* [harbor config get](harbor-config-get.md) - Get a specific config item
|
||||
* [harbor config list](harbor-config-list.md) - List config items
|
||||
* [harbor config set](harbor-config-set.md) - Set a specific config item
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
title: harbor config delete
|
||||
title: harbor context delete
|
||||
weight: 85
|
||||
---
|
||||
## harbor config delete
|
||||
## harbor context delete
|
||||
|
||||
### Description
|
||||
|
||||
|
@ -15,7 +15,7 @@ Case-insensitive field lookup, but uses the canonical (Go) field name internally
|
|||
If you specify --name, that credential (rather than the "current" one) will be used.
|
||||
|
||||
```sh
|
||||
harbor config delete <item> [flags]
|
||||
harbor context delete <item> [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
@ -23,16 +23,20 @@ harbor config delete <item> [flags]
|
|||
```sh
|
||||
|
||||
# Clear the current credential's password
|
||||
harbor config delete credentials.password
|
||||
harbor context delete credentials.password
|
||||
|
||||
# Clear a specific credential's password using --name
|
||||
harbor config delete credentials.password --name harbor-cli@http://demo.goharbor.io
|
||||
harbor context delete credentials.password --name admin@http://demo.goharbor.io
|
||||
|
||||
# Clear the current credential
|
||||
harbor context delete --current
|
||||
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```sh
|
||||
--current Remove current credentials from the config
|
||||
-h, --help help for delete
|
||||
-n, --name string Name of the credential to delete fields from (default: the current credential)
|
||||
```
|
||||
|
@ -47,5 +51,5 @@ harbor config delete <item> [flags]
|
|||
|
||||
### SEE ALSO
|
||||
|
||||
* [harbor config](harbor-config.md) - Manage the config of the Harbor Cli
|
||||
* [harbor context](harbor-context.md) - Manage locally available contexts
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
title: harbor config get
|
||||
title: harbor context get
|
||||
weight: 85
|
||||
---
|
||||
## harbor config get
|
||||
## harbor context get
|
||||
|
||||
### Description
|
||||
|
||||
|
@ -14,7 +14,7 @@ Get the value of a specific CLI config item.
|
|||
If you specify --name, that credential (rather than the "current" one) will be used.
|
||||
|
||||
```sh
|
||||
harbor config get <item> [flags]
|
||||
harbor context get <item> [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
@ -22,10 +22,10 @@ harbor config get <item> [flags]
|
|||
```sh
|
||||
|
||||
# Get the current credential's username
|
||||
harbor config get credentials.username
|
||||
harbor context get credentials.username
|
||||
|
||||
# Get a credential's username by specifying the credential name
|
||||
harbor config get credentials.username --name harbor-cli@http://demo.goharbor.io
|
||||
harbor config get credentials.username --name admin@http://demo.goharbor.io
|
||||
|
||||
```
|
||||
|
||||
|
@ -46,5 +46,5 @@ harbor config get <item> [flags]
|
|||
|
||||
### SEE ALSO
|
||||
|
||||
* [harbor config](harbor-config.md) - Manage the config of the Harbor Cli
|
||||
* [harbor context](harbor-context.md) - Manage locally available contexts
|
||||
|
|
@ -1,25 +1,21 @@
|
|||
---
|
||||
title: harbor config list
|
||||
title: harbor context list
|
||||
weight: 25
|
||||
---
|
||||
## harbor config list
|
||||
## harbor context list
|
||||
|
||||
### Description
|
||||
|
||||
##### List config items
|
||||
|
||||
### Synopsis
|
||||
|
||||
Get information of all CLI config items
|
||||
##### List contexts
|
||||
|
||||
```sh
|
||||
harbor config list [flags]
|
||||
harbor context list [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```sh
|
||||
harbor config list
|
||||
harbor context list
|
||||
```
|
||||
|
||||
### Options
|
||||
|
@ -38,5 +34,5 @@ harbor config list [flags]
|
|||
|
||||
### SEE ALSO
|
||||
|
||||
* [harbor config](harbor-config.md) - Manage the config of the Harbor Cli
|
||||
* [harbor context](harbor-context.md) - Manage locally available contexts
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
title: harbor context switch
|
||||
weight: 50
|
||||
---
|
||||
## harbor context switch
|
||||
|
||||
### Description
|
||||
|
||||
##### Switch to a new context
|
||||
|
||||
```sh
|
||||
harbor context switch <none|context> [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```sh
|
||||
harbor context switch harbor-cli@https-demo-goharbor-io
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```sh
|
||||
-h, --help help for switch
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```sh
|
||||
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
|
||||
-o, --output-format string Output format. One of: json|yaml
|
||||
-v, --verbose verbose output
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [harbor context](harbor-context.md) - Manage locally available contexts
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
title: harbor config update
|
||||
title: harbor context update
|
||||
weight: 10
|
||||
---
|
||||
## harbor config update
|
||||
## harbor context update
|
||||
|
||||
### Description
|
||||
|
||||
|
@ -15,7 +15,7 @@ Case-insensitive field lookup, but uses the canonical (Go) field name internally
|
|||
If you specify --name, that credential (rather than the "current" one) will be updated.
|
||||
|
||||
```sh
|
||||
harbor config update <item> <value> [flags]
|
||||
harbor context update <item> <value> [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
@ -23,7 +23,7 @@ harbor config update <item> <value> [flags]
|
|||
```sh
|
||||
|
||||
# Set/update the current credential's password
|
||||
harbor config update credentials.password myNewSecret
|
||||
harbor context update credentials.password myNewSecret
|
||||
|
||||
# Set/update a credential's password by specifying the credential name
|
||||
harbor config update credentials.password myNewSecret --name admin@http://demo.goharbor.io
|
||||
|
@ -47,5 +47,5 @@ harbor config update <item> <value> [flags]
|
|||
|
||||
### SEE ALSO
|
||||
|
||||
* [harbor config](harbor-config.md) - Manage the config of the Harbor Cli
|
||||
* [harbor context](harbor-context.md) - Manage locally available contexts
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
title: harbor context
|
||||
weight: 30
|
||||
---
|
||||
## harbor context
|
||||
|
||||
### Description
|
||||
|
||||
##### Manage locally available contexts
|
||||
|
||||
### Synopsis
|
||||
|
||||
The context command allows you to manage configuration items of the Harbor CLI.
|
||||
You can add, get, or delete specific configuration items, as well as list all configuration items of the Harbor CLI.
|
||||
|
||||
### Examples
|
||||
|
||||
```sh
|
||||
harbor context list
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```sh
|
||||
-h, --help help for context
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```sh
|
||||
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
|
||||
-o, --output-format string Output format. One of: json|yaml
|
||||
-v, --verbose verbose output
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [harbor](harbor.md) - Official Harbor CLI
|
||||
* [harbor context delete](harbor-context-delete.md) - Delete (clear) a specific config item
|
||||
* [harbor context get](harbor-context-get.md) - Get a specific config item
|
||||
* [harbor context list](harbor-context-list.md) - List contexts
|
||||
* [harbor context switch](harbor-context-switch.md) - Switch to a new context
|
||||
* [harbor context update](harbor-context-update.md) - Set/update a specific config item
|
||||
|
|
@ -10,7 +10,7 @@ weight: 60
|
|||
|
||||
### Synopsis
|
||||
|
||||
Create allowlists of CVEs to ignore during vulnerability scanning
|
||||
Create allowlist of CVEs to ignore during vulnerability scanning
|
||||
|
||||
```sh
|
||||
harbor cve-allowlist add [flags]
|
||||
|
|
|
@ -6,7 +6,7 @@ weight: 60
|
|||
|
||||
### Description
|
||||
|
||||
##### list system level allowlist of cve
|
||||
##### List system level allowlist of cve
|
||||
|
||||
```sh
|
||||
harbor cve-allowlist list [flags]
|
||||
|
|
|
@ -36,5 +36,5 @@ harbor cve-allowlist list
|
|||
|
||||
* [harbor](harbor.md) - Official Harbor CLI
|
||||
* [harbor cve-allowlist add](harbor-cve-allowlist-add.md) - Add cve allowlist
|
||||
* [harbor cve-allowlist list](harbor-cve-allowlist-list.md) - list system level allowlist of cve
|
||||
* [harbor cve-allowlist list](harbor-cve-allowlist-list.md) - List system level allowlist of cve
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ harbor health [flags]
|
|||
### Options inherited from parent commands
|
||||
|
||||
```sh
|
||||
--config string config file (default is $HOME/.harbor/config.yaml) (default "/Users/vadim/.harbor/config.yaml")
|
||||
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
|
||||
-o, --output-format string Output format. One of: json|yaml
|
||||
-v, --verbose verbose output
|
||||
```
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: harbor instance create
|
||||
weight: 0
|
||||
weight: 10
|
||||
---
|
||||
## harbor instance create
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ harbor instance delete [flags]
|
|||
|
||||
```sh
|
||||
-h, --help help for delete
|
||||
-i, --id int ID of the instance to delete (default -1)
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: harbor label delete
|
||||
weight: 0
|
||||
weight: 30
|
||||
---
|
||||
## harbor label delete
|
||||
|
||||
|
|
|
@ -20,15 +20,15 @@ harbor login [server] [flags]
|
|||
|
||||
```sh
|
||||
-h, --help help for login
|
||||
--name string name for the set of credentials
|
||||
-p, --password string Password
|
||||
--password-stdin Take the password from stdin
|
||||
-u, --username string Username
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```sh
|
||||
--config string config file (default is $HOME/.harbor/config.yaml) (default "/home/user/.harbor/config.yaml")
|
||||
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
|
||||
-o, --output-format string Output format. One of: json|yaml
|
||||
-v, --verbose verbose output
|
||||
```
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
title: harbor logs
|
||||
weight: 30
|
||||
---
|
||||
## harbor logs
|
||||
|
||||
### Description
|
||||
|
||||
##### Get recent logs of the projects which the user is a member of
|
||||
|
||||
### Synopsis
|
||||
|
||||
Get recent logs of the projects which the user is a member of.
|
||||
This command retrieves the audit logs for the projects the user is a member of. It supports pagination, sorting, and filtering through query parameters. The logs can be followed in real-time with the --follow flag, and the output can be formatted as JSON with the --output-format flag.
|
||||
|
||||
harbor-cli logs --page 1 --page-size 10 --query "operation=push" --sort "op_time:desc"
|
||||
|
||||
harbor-cli logs --follow --refresh-interval 2s
|
||||
|
||||
harbor-cli logs --output-format json
|
||||
|
||||
```sh
|
||||
harbor logs [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```sh
|
||||
-f, --follow Follow log output (tail -f behavior)
|
||||
-h, --help help for logs
|
||||
--page int Page number (default 1)
|
||||
--page-size int Size of per page (default 10)
|
||||
-q, --query string Query string to query resources
|
||||
-n, --refresh-interval string Interval to refresh logs when following (default: 5s)
|
||||
--sort string Sort the resource list in ascending or descending order
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```sh
|
||||
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
|
||||
-o, --output-format string Output format. One of: json|yaml
|
||||
-v, --verbose verbose output
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [harbor](harbor.md) - Official Harbor CLI
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
title: harbor project config list
|
||||
weight: 85
|
||||
---
|
||||
## harbor project config list
|
||||
|
||||
### Description
|
||||
|
||||
##### List configuration of a Harbor project by name or ID
|
||||
|
||||
### Synopsis
|
||||
|
||||
Display the configuration metadata of a Harbor project specified by its name or ID.
|
||||
|
||||
If no project name or ID is provided as an argument, you will be prompted to select a project interactively.
|
||||
|
||||
You can use the global flag '--output-format' to specify the output format, e.g. 'json' or 'yaml', for machine-readable output.
|
||||
|
||||
Examples:
|
||||
|
||||
# List configuration of project 'myproject' by name
|
||||
harbor-cli project config list myproject
|
||||
|
||||
# List configuration of project with ID '123'
|
||||
harbor-cli project config list 123
|
||||
|
||||
# Run interactively (prompt to select project)
|
||||
harbor-cli project config list
|
||||
|
||||
# List config in JSON format
|
||||
harbor-cli project config list myproject --output-format json
|
||||
|
||||
|
||||
```sh
|
||||
harbor project config list [project_name] [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```sh
|
||||
-h, --help help for list
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```sh
|
||||
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
|
||||
--id Use project ID instead of name
|
||||
-o, --output-format string Output format. One of: json|yaml
|
||||
-v, --verbose verbose output
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [harbor project config](harbor-project-config.md) - Manage project configuration
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
title: harbor project config update
|
||||
weight: 10
|
||||
---
|
||||
## harbor project config update
|
||||
|
||||
### Description
|
||||
|
||||
##### Interactively or via flags update project configuration in Harbor
|
||||
|
||||
### Synopsis
|
||||
|
||||
Update the configuration settings of a Harbor project either interactively or directly using command-line flags.
|
||||
|
||||
You can specify the project by its name or ID as an argument. If not provided, you will be prompted to select a project interactively.
|
||||
|
||||
Examples:
|
||||
|
||||
# Update project 'myproject' visibility to public
|
||||
harbor-cli project config update myproject --public true
|
||||
|
||||
# Update multiple settings in one command
|
||||
harbor-cli project config update myproject --public false --prevent-vul true --severity high
|
||||
|
||||
# Run interactively without flags
|
||||
harbor-cli project config update
|
||||
|
||||
Supported flag values:
|
||||
|
||||
- Boolean flags (public, auto-scan, prevent-vul, reuse-sys-cve-allowlist, enable-content-trust, enable-content-trust-cosign): "true" or "false"
|
||||
- Severity: one of "low", "medium", "high", "critical"
|
||||
|
||||
|
||||
```sh
|
||||
harbor project config update [project_name] [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```sh
|
||||
--auto-scan string Enable or disable auto scan (true/false)
|
||||
--enable-content-trust string Enable or disable content trust (true/false)
|
||||
--enable-content-trust-cosign string Enable or disable content trust cosign (true/false)
|
||||
-h, --help help for update
|
||||
--prevent-vul string Enable or disable vulnerability prevention (true/false)
|
||||
--public string Set project visibility (true/false)
|
||||
--reuse-sys-cve string Enable or disable reuse of system CVE allowlist (true/false)
|
||||
--severity string Set severity level
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```sh
|
||||
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
|
||||
--id Use project ID instead of name
|
||||
-o, --output-format string Output format. One of: json|yaml
|
||||
-v, --verbose verbose output
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [harbor project config](harbor-project-config.md) - Manage project configuration
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
title: harbor project config
|
||||
weight: 45
|
||||
---
|
||||
## harbor project config
|
||||
|
||||
### Description
|
||||
|
||||
##### Manage project configuration
|
||||
|
||||
### Options
|
||||
|
||||
```sh
|
||||
-h, --help help for config
|
||||
--id Use project ID instead of name
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```sh
|
||||
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
|
||||
-o, --output-format string Output format. One of: json|yaml
|
||||
-v, --verbose verbose output
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [harbor project](harbor-project.md) - Manage projects and assign resources to them
|
||||
* [harbor project config list](harbor-project-config-list.md) - List configuration of a Harbor project by name or ID
|
||||
* [harbor project config update](harbor-project-config-update.md) - Interactively or via flags update project configuration in Harbor
|
||||
|
|
@ -9,24 +9,23 @@ weight: 80
|
|||
##### create project
|
||||
|
||||
```sh
|
||||
harbor project create [flags]
|
||||
harbor project create [project name] [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```sh
|
||||
-h, --help help for create
|
||||
--name string Name of the project
|
||||
--proxy-cache Whether the project is a proxy cache project
|
||||
--public Project is public or private (default true)
|
||||
--public Project is public or private
|
||||
--registry-id string ID of referenced registry when creating the proxy cache project
|
||||
--storage-limit string Storage quota of the project (default "-1")
|
||||
--storage-limit string Storage quota of the project
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```sh
|
||||
--config string config file (default is $HOME/.harbor/config.yaml) (default "/home/user/.harbor/config.yaml")
|
||||
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
|
||||
-o, --output-format string Output format. One of: json|yaml
|
||||
-v, --verbose verbose output
|
||||
```
|
||||
|
|
|
@ -6,22 +6,34 @@ weight: 100
|
|||
|
||||
### Description
|
||||
|
||||
##### delete project by name or id
|
||||
##### Delete project by name or ID
|
||||
|
||||
### Synopsis
|
||||
|
||||
Delete project by name or ID. Multiple projects can be deleted by providing their names as arguments. If no arguments are provided, it will prompt for the project name. Use --project-id to specify the project ID for single project directly. The --force flag will delete all repositories and artifacts within the project.
|
||||
|
||||
```sh
|
||||
harbor project delete [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```sh
|
||||
harbor project delete [projectname1] [projectname2] or harbor project delete --project-id [projectid]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```sh
|
||||
--force Forcefully delete all repositories, artifacts, and policies in the project. Use with extreme caution—this action is irreversible.
|
||||
-h, --help help for delete
|
||||
--project-id string Specify project ID instead of project name
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```sh
|
||||
--config string config file (default is $HOME/.harbor/config.yaml) (default "/home/user/.harbor/config.yaml")
|
||||
-c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml)
|
||||
-o, --output-format string Output format. One of: json|yaml
|
||||
-v, --verbose verbose output
|
||||
```
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue