зеркало из https://github.com/Azure/azure-dev.git
Moved extensions and sdk into azure-dev
This commit is contained in:
Родитель
b252a17b2a
Коммит
8fa9a3f57a
|
@ -0,0 +1 @@
|
|||
*.log
|
|
@ -1,11 +1,11 @@
|
|||
module github.com/azure/azure-dev
|
||||
module github.com/azure/azure-dev/cli/azd
|
||||
|
||||
go 1.23
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.2
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/apimanagement/armapimanagement v1.0.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appconfiguration/armappconfiguration v1.0.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appcontainers/armappcontainers/v3 v3.0.0-beta.1
|
||||
|
@ -13,7 +13,7 @@ require (
|
|||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appservice/armappservice/v2 v2.3.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization v1.0.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.1.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices v1.4.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices v1.6.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v0.6.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v2 v2.2.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos/v2 v2.6.0
|
||||
|
@ -21,12 +21,12 @@ require (
|
|||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/machinelearning/armmachinelearning/v3 v3.2.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.7.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armdeploymentstacks v1.0.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.0.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2 v2.0.0-beta.4
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v0.13.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azfile v1.2.2
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azfile v1.3.1
|
||||
github.com/Azure/azure-storage-file-go v0.8.0
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2
|
||||
github.com/MakeNowJust/heredoc/v2 v2.0.1
|
||||
|
@ -38,59 +38,59 @@ require (
|
|||
github.com/buger/goterm v1.0.4
|
||||
github.com/cli/browser v1.1.0
|
||||
github.com/drone/envsubst v1.0.3
|
||||
github.com/fatih/color v1.13.0
|
||||
github.com/fatih/color v1.17.0
|
||||
github.com/gofrs/flock v0.8.1
|
||||
github.com/golobby/container/v3 v3.3.1
|
||||
github.com/golobby/container/v3 v3.3.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/joho/godotenv v1.4.0
|
||||
github.com/magefile/mage v1.12.1
|
||||
github.com/mattn/go-colorable v0.1.12
|
||||
github.com/mattn/go-isatty v0.0.14
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mattn/go-colorable v0.1.13
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/microsoft/ApplicationInsights-Go v0.4.4
|
||||
github.com/microsoft/azure-devops-go-api/azuredevops/v7 v7.1.0
|
||||
github.com/microsoft/go-deviceid v1.0.0
|
||||
github.com/moby/patternmatcher v0.6.0
|
||||
github.com/nathan-fiscaletti/consolesize-go v0.0.0-20220204101620-317176b6684d
|
||||
github.com/otiai10/copy v1.9.0
|
||||
github.com/psanford/memfs v0.0.0-20230130182539-4dbf7e3e865e
|
||||
github.com/psanford/memfs v0.0.0-20240922203233-02fb08c0f8db
|
||||
github.com/sethvargo/go-retry v0.2.3
|
||||
github.com/spf13/cobra v1.3.0
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/theckman/yacspin v0.13.12
|
||||
go.lsp.dev/jsonrpc2 v0.10.0
|
||||
go.opentelemetry.io/otel v1.8.0
|
||||
go.opentelemetry.io/otel v1.30.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.8.0
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.8.0
|
||||
go.opentelemetry.io/otel/sdk v1.8.0
|
||||
go.opentelemetry.io/otel/trace v1.8.0
|
||||
go.uber.org/atomic v1.9.0
|
||||
go.opentelemetry.io/otel/sdk v1.30.0
|
||||
go.opentelemetry.io/otel/trace v1.30.0
|
||||
go.uber.org/atomic v1.11.0
|
||||
go.uber.org/multierr v1.8.0
|
||||
golang.org/x/sys v0.21.0
|
||||
golang.org/x/sys v0.26.0
|
||||
gopkg.in/dnaeon/go-vcr.v3 v3.1.2
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-pipeline-go v0.2.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.1.2 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/go-logr/logr v1.2.3 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/segmentio/asm v1.1.3 // indirect
|
||||
|
@ -98,11 +98,12 @@ require (
|
|||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.8.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.8.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.30.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v0.18.0 // indirect
|
||||
golang.org/x/crypto v0.24.0 // indirect
|
||||
golang.org/x/net v0.26.0 // indirect
|
||||
golang.org/x/term v0.21.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/crypto v0.28.0 // indirect
|
||||
golang.org/x/net v0.30.0 // indirect
|
||||
golang.org/x/term v0.25.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
||||
google.golang.org/grpc v1.56.3 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
|
@ -13,20 +13,6 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV
|
|||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
|
||||
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
|
||||
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
|
||||
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
|
||||
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
|
||||
cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
|
||||
cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
|
||||
cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
|
||||
cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
|
||||
cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
|
||||
cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
|
||||
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
|
||||
cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM=
|
||||
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
|
@ -35,7 +21,6 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g
|
|||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
|
@ -51,12 +36,10 @@ github.com/AlecAivazis/survey/v2 v2.3.2 h1:TqTB+aDDCLYhf9/bD2TwSO8u8jDSmMUd2SUVO
|
|||
github.com/AlecAivazis/survey/v2 v2.3.2/go.mod h1:TH2kPCDU3Kqq7pLbnCWwZXDBjnhZtmsCle5EiYDJ2fg=
|
||||
github.com/Azure/azure-pipeline-go v0.2.1 h1:OLBdZJ3yvOn2MezlWvbrBMTEUQC72zAftRZOMdj5HYo=
|
||||
github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0 h1:1nGuui+4POelzDwI7RG56yfQJHCnKvwfMoU7VsEp+Zg=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0/go.mod h1:99EvauvlcJ1U06amZiksfYz/3aFGyIhWGHVyiZXtBAI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0 h1:H+U3Gk9zY56G3u872L82bk4thcsy2Gghb9ExT4Zvm1o=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0/go.mod h1:mgrmMSgaLp9hmax62XQTd0N4aAqSE5E0DulSpVYK7vc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 h1:nyQWyZvwGTvunIMxi1Y9uXkcyr+I7TeNrr/foo4Kpk8=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0 h1:+m0M/LFxN43KvULkDNfdXOgrjtg6UYJPFBJyuEcRCAw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/apimanagement/armapimanagement v1.0.0 h1:Ai3+BE11JvwQ2PxLGNKAfMNSceYXjeijReLJiCouO6o=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/apimanagement/armapimanagement v1.0.0/go.mod h1:gr6fiHmIii3Zw3riWMSr+P0tWTz4hfqTVcFttdi2JBo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appconfiguration/armappconfiguration v1.0.0 h1:5reBX+9pzc5xp9VrjSUoPrE8Wl/3y7wjfHzGjXzJbNk=
|
||||
|
@ -71,8 +54,7 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthoriza
|
|||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization v1.0.0/go.mod h1:lPneRe3TwsoDRKY4O6YDLXHhEWrD+TIRa8XrV/3/fqw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.1.1 h1:6A4M8smF+y8nM/DYsLNQz9n7n2ZGaEVqfz8ZWQirQkI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.1.1/go.mod h1:WqyxV5S0VtXD2+2d6oPqOvyhGubCvzLCKSAKgQ004Uk=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices v1.4.1 h1:ynIxbR7wH5nBEJzprbeBFVBtoYTYcQbN39vM7eNS3Xc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices v1.4.1/go.mod h1:nUhnLNlOtAVpn/PRwJKIf3ulXLvdMiWlGk8nufEUaKc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices v1.6.0 h1:TiYjDq0LCNgtee1teMayYT5FjHmlunWUpthVANUXYPM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v0.6.0 h1:Z5/bDxQL2Zc9t6ZDwdRU60bpLHZvoKOeuaM7XVbf2z0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v0.6.0/go.mod h1:0FPu3oDRGPvuX1H8TtHJ5XGA0KrXLunomcixR+PQGGA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v2 v2.2.0 h1:3L+gX5ssCABAToH0VQ64/oNz7rr+ShW+2sB+sonzIlY=
|
||||
|
@ -93,29 +75,24 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourceg
|
|||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.7.1/go.mod h1:21rlzm+SuYrS9ARS92XEGxcHQeLVDcaY2YV30rHjSd4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armdeploymentstacks v1.0.0 h1:lbA8Oh+TM2s7d68yz1hbtysTNpcYuBDsgGdCp03fgNw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armdeploymentstacks v1.0.0/go.mod h1:40Esvrf2NihfFyyoM0lDUCJfEbI6ZxkR7iCwY1IzRAU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1 h1:7CBQ+Ei8SP2c6ydQTGCCrS35bDxgTMfoP2miAwK++OU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1/go.mod h1:c/wcGeGx5FUPbM/JltUYHZcKmigwyVLJlDq+4HdtXaw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.0.0 h1:xXmHA6JxGDHOY2anNQhpgIibZOiEaOvPLZOiAs07/4k=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.0.0/go.mod h1:qkZjuhvy20x2Ckq4BzopZ8UjZLhib6nRJbRQiC6EFXY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0 h1:wxQx2Bt4xzPIKvW59WQf1tJNx/ZZKPfN+EhPX3Z6CYY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2 v2.0.0-beta.4 h1:REtDJygCJQ4IkwgSOAY3CUpI1+AWKuOVP3jIGPYYiPk=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2 v2.0.0-beta.4/go.mod h1:mXe/tbvI454sWulmm+3N5fkEs+yDvbQsA+8Xqr1kEQo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.5.0 h1:AifHbc4mg0x9zW52WOpKbsHaDKuRhlI7TVl47thgQ70=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.5.0/go.mod h1:T5RfihdXtBDxt1Ch2wobif3TvzTdumDy29kahv6AV9A=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v0.13.0 h1:XY0plaTx8oeipK+XogAck2Qzv39KdnJNBwrxC4A0GL4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v0.13.0/go.mod h1:tj2JhpZY+NjcQcZ207YHkfwYuivmTrcj5ZNpQxpT3Qk=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0 h1:T028gtTPiYt/RMUfs8nVsAL7FDQrfLlrm/NnRG/zcC4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0/go.mod h1:cw4zVQgBby0Z5f2v0itn6se2dDP17nTjbZFXW5uPyHA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.1 h1:fXPMAmuh0gDuRDey0atC8cXBuKIlqCzCkL8sm1n9Ov0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.1/go.mod h1:SUZc9YRRHfx2+FAQKNDGrssXehqLpxmwRv2mC/5ntj4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azfile v1.2.2 h1:PmDhkIT8S5U4nkY/s78Xmf7CXT8qCliNEBhbrkBp3Q0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azfile v1.2.2/go.mod h1:Kj2pCkQ47klX1aAlDnlN/BUvwBiARqIJkc9iw1Up7q8=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.1 h1:cf+OIKbkmMHBaC3u78AXomweqM0oxQSgBXRZf3WH4yM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azfile v1.3.1 h1:a1U6j4GPI18JQCqgz7/DcqXA1vzvGBugm14AXZfU0gs=
|
||||
github.com/Azure/azure-storage-file-go v0.8.0 h1:OX8DGsleWLUE6Mw4R/OeWEZMvsTIpwN94J59zqKQnTI=
|
||||
github.com/Azure/azure-storage-file-go v0.8.0/go.mod h1:3w3mufGcMjcOJ3w+4Gs+5wsSgkT7xDwWWqMMIrXtW4c=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/MakeNowJust/heredoc/v2 v2.0.1 h1:rlCHh70XXXv7toz95ajQWOWQnN4WNLt0TdpZYIR/J6A=
|
||||
github.com/MakeNowJust/heredoc/v2 v2.0.1/go.mod h1:6/2Abh5s+hc3g9nbWLe9ObDIOhaRrqsyY9MWy+4JdRM=
|
||||
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
|
||||
|
@ -123,22 +100,9 @@ github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61
|
|||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/adam-lavrik/go-imath v0.0.0-20210910152346-265a42a96f0b h1:g9SuFmxM/WucQFKTMSP+irxyf5m0RiUJreBDhGI6jSA=
|
||||
github.com/adam-lavrik/go-imath v0.0.0-20210910152346-265a42a96f0b/go.mod h1:XjvqMUpGd3Xn9Jtzk/4GEBCSoBX0eB2RyriXgne0IdM=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
|
||||
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.0 h1:HTuxyug8GyFbRkrffIpzNCSK4luc0TY3wzXvzIZhEXc=
|
||||
|
@ -150,73 +114,49 @@ github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQt
|
|||
github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4=
|
||||
github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||
github.com/cli/browser v1.1.0 h1:xOZBfkfY9L9vMBgqb1YwRirGu6QFaQ5dP/vXt5ENSOY=
|
||||
github.com/cli/browser v1.1.0/go.mod h1:HKMQAt9t12kov91Mn7RfZxyJQQgWgyS/3SZswlZ5iTI=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
|
||||
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g=
|
||||
github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
||||
github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
|
||||
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
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/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
|
@ -226,7 +166,6 @@ github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP
|
|||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
|
@ -234,8 +173,6 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
|
|||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
|
@ -251,13 +188,10 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
|
|||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golobby/container/v3 v3.3.1 h1:Y+QpwChmkz86tAKimvqc0qls8A4eYm/PhMqSEt/HTj4=
|
||||
github.com/golobby/container/v3 v3.3.1/go.mod h1:RDdKpnKpV1Of11PFBe7Dxc2C1k2KaLE4FD47FflAmj0=
|
||||
github.com/golobby/container/v3 v3.3.2 h1:7u+RgNnsdVlhGoS8gY4EXAG601vpMMzLZlYqSp77Quw=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
|
@ -267,18 +201,13 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
|
@ -286,13 +215,6 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf
|
|||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
|
@ -300,69 +222,26 @@ 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/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
|
||||
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 h1:BZHcxBETFHIdVyhyEfOvn/RdU/QGdLI4y34qQGjGWO0=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
|
||||
github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
|
||||
github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=
|
||||
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
|
||||
github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
|
||||
github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
|
||||
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
|
||||
github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
|
||||
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
|
||||
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
|
@ -374,29 +253,14 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
|
||||
github.com/magefile/mage v1.12.1 h1:oGdAbhIUd6iKamKlDGVtU6XGdy5SgNuCWn7gCTgHDtU=
|
||||
github.com/magefile/mage v1.12.1/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149 h1:HfxbT6/JcvIljmERptWhwa8XzP7H3T+Z2N26gTsaDaA=
|
||||
github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/microsoft/ApplicationInsights-Go v0.4.4 h1:G4+H9WNs6ygSCe6sUyxRc2U81TI5Es90b2t/MwX5KqY=
|
||||
|
@ -405,23 +269,8 @@ github.com/microsoft/azure-devops-go-api/azuredevops/v7 v7.1.0 h1:mmJCWLe63Qvybx
|
|||
github.com/microsoft/azure-devops-go-api/azuredevops/v7 v7.1.0/go.mod h1:mDunUZ1IUJdJIRHvFb+LPBUtxe3AYB5MI6BMXNg8194=
|
||||
github.com/microsoft/go-deviceid v1.0.0 h1:i5AQ654Xk9kfvwJeKQm3w2+eT1+ImBDVEpAR0AjpP40=
|
||||
github.com/microsoft/go-deviceid v1.0.0/go.mod h1:KY13FeVdHkzD8gy+6T8+kVmD/7RMpTaWW75K+T4uZWg=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
||||
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nathan-fiscaletti/consolesize-go v0.0.0-20220204101620-317176b6684d h1:NqRhLdNVlozULwM1B3VaHhcXYSgrOAv8V5BE65om+1Q=
|
||||
github.com/nathan-fiscaletti/consolesize-go v0.0.0-20220204101620-317176b6684d/go.mod h1:cxIIfNMTwff8f/ZvRouvWYF6wOoO7nj99neWSx2q/Es=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
|
@ -434,33 +283,14 @@ github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6
|
|||
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
|
||||
github.com/otiai10/mint v1.4.0 h1:umwcf7gbpEwf7WFzqmWwSv0CzbeMsae2u9ZvpP8j2q4=
|
||||
github.com/otiai10/mint v1.4.0/go.mod h1:gifjb2MYOoULtKLqUAEILUG/9KONW6f7YsJ6vQLTlFI=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/psanford/memfs v0.0.0-20230130182539-4dbf7e3e865e h1:51xcRlSMBU5rhM9KahnJGfEsBPVPz3182TgFRowA8yY=
|
||||
github.com/psanford/memfs v0.0.0-20230130182539-4dbf7e3e865e/go.mod h1:tcaRap0jS3eifrEEllL6ZMd9dg8IlDpi2S1oARrQ+NI=
|
||||
github.com/psanford/memfs v0.0.0-20240922203233-02fb08c0f8db h1:uwKfJcTGnZ4ziFqr0bjzNc/P1fydrAg450b9Dz8Fj8M=
|
||||
github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
|
@ -468,27 +298,16 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
|
|||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
|
||||
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
|
||||
github.com/segmentio/encoding v0.3.4 h1:WM4IBnxH8B9TakiM2QD5LyNl9JSndh88QbHqVC+Pauc=
|
||||
github.com/segmentio/encoding v0.3.4/go.mod h1:n0JeuIqEQrQoPDGsjo8UNd1iA0U8d8+oHAA4E3G3OxM=
|
||||
github.com/sethvargo/go-retry v0.2.3 h1:oYlgvIvsju3jNbottWABtbnoLC+GDtLdBHxKWxQm/iU=
|
||||
github.com/sethvargo/go-retry v0.2.3/go.mod h1:1afjQuvh7s4gflMObvjLPaWgluLLyhA1wmVZ6KLpICw=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
|
||||
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
||||
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v1.3.0 h1:R7cSvGu+Vv+qX0gW5R/85dx2kmmJT5z5NM8ifdYjdn0=
|
||||
github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
|
@ -496,7 +315,6 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
|
|||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
|
@ -507,19 +325,12 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
|||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tedsuo/ifrit v0.0.0-20180802180643-bea94bb476cc/go.mod h1:eyZnKCc955uh98WQvzOm0dgAeLnf2O0Rz0LPoC5ze+0=
|
||||
github.com/theckman/yacspin v0.13.12 h1:CdZ57+n0U6JMuh2xqjnjRq5Haj6v1ner2djtLQRzJr4=
|
||||
github.com/theckman/yacspin v0.13.12/go.mod h1:Rd2+oG2LmQi5f3zC3yeZAOl245z8QOvrH4OPOJNZxLg=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
|
||||
go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs=
|
||||
go.lsp.dev/jsonrpc2 v0.10.0 h1:Pr/YcXJoEOTMc/b6OTmcR1DPJ3mSWl/SWiU1Cct6VmI=
|
||||
go.lsp.dev/jsonrpc2 v0.10.0/go.mod h1:fmEzIdXPi/rf6d4uFcayi8HpFP1nBF99ERP1htC72Ac=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
|
@ -527,10 +338,7 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
|||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
|
||||
go.opentelemetry.io/otel v1.8.0 h1:zcvBFizPbpa1q7FehvFiHbQwGzmPILebO0tyqIR5Djg=
|
||||
go.opentelemetry.io/otel v1.8.0/go.mod h1:2pkj+iMj0o03Y+cW6/m8Y4WkRdYN3AvCXCnzRMp9yvM=
|
||||
go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts=
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.8.0 h1:ao8CJIShCaIbaMsGxy+jp2YHSudketpDgDRcbirov78=
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.8.0/go.mod h1:78XhIg8Ht9vR4tbLNUhXsiOnE2HOuSeKAiAcoVQEpOY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.8.0 h1:LrHL1A3KqIgAgi6mK7Q0aczmzU414AONAGT5xtnp+uo=
|
||||
|
@ -539,33 +347,23 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.8.0 h1:SMO1Ho
|
|||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.8.0/go.mod h1:tsw+QO2+pGo7xOrPXrS27HxW8uqGQkw5AzJwdsoyvgw=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.8.0 h1:FVy7BZCjoA2Nk+fHqIdoTmm554J9wTX+YcrDp+mc368=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.8.0/go.mod h1:ztncjvKpotSUQq7rlgPibGt8kZfSI3/jI8EO7JjuY2c=
|
||||
go.opentelemetry.io/otel/sdk v1.8.0 h1:xwu69/fNuwbSHWe/0PGS888RmjWY181OmcXDQKu7ZQk=
|
||||
go.opentelemetry.io/otel/sdk v1.8.0/go.mod h1:uPSfc+yfDH2StDM/Rm35WE8gXSNdvCg023J6HeGNO0c=
|
||||
go.opentelemetry.io/otel/trace v1.8.0 h1:cSy0DF9eGI5WIfNwZ1q2iUyGj00tGzP24dE1lOlHrfY=
|
||||
go.opentelemetry.io/otel/trace v1.8.0/go.mod h1:0Bt3PXY8w+3pheS3hQUt+wow8b1ojPaTBoTCh2zIFI4=
|
||||
go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w=
|
||||
go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE=
|
||||
go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.opentelemetry.io/proto/otlp v0.18.0 h1:W5hyXNComRa23tGpKwG+FRAc4rfF6ZUg1JReK+QHS80=
|
||||
go.opentelemetry.io/proto/otlp v0.18.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
|
||||
go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
|
||||
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
|
@ -588,8 +386,6 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl
|
|||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
|
@ -598,15 +394,9 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
|
|||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
|
@ -614,11 +404,9 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
|||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
|
@ -633,35 +421,13 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
|
|||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
||||
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
@ -671,37 +437,23 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -713,57 +465,26 @@ golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
|
||||
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
@ -780,7 +501,6 @@ golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgw
|
|||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
|
@ -805,22 +525,9 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY
|
|||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
@ -841,29 +548,12 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M
|
|||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
||||
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
|
||||
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
|
||||
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
|
||||
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
|
||||
google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
|
||||
google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
|
||||
google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
|
||||
google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
|
||||
google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
|
||||
google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
|
||||
google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
|
||||
google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
|
||||
google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=
|
||||
google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
|
||||
google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
|
@ -894,39 +584,7 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc
|
|||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
|
||||
google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
|
||||
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||
google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||
google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
|
||||
google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
|
||||
google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
|
||||
google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
|
||||
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
|
||||
google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
|
||||
google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
||||
google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
||||
google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
||||
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
||||
google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
||||
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
|
@ -941,24 +599,12 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
|
|||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
|
||||
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
|
||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
||||
google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
|
||||
google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
|
@ -974,25 +620,18 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
|
|||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/dnaeon/go-vcr.v3 v3.1.2 h1:F1smfXBqQqwpVifDfUBQG6zzaGjzT+EnVZakrOdr5wA=
|
||||
gopkg.in/dnaeon/go-vcr.v3 v3.1.2/go.mod h1:2IMOnnlx9I6u9x+YBsM3tAMx6AlOxnJ0pWxQAzZ79Ag=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
@ -0,0 +1,25 @@
|
|||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# env file
|
||||
.env
|
|
@ -0,0 +1,3 @@
|
|||
# azd ai extension
|
||||
|
||||
An AZD AI extension
|
|
@ -0,0 +1,51 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Get the directory of the script
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
# Change to the script directory
|
||||
cd "$SCRIPT_DIR" || exit
|
||||
|
||||
# Define application name
|
||||
APP_NAME="azd-ext-ai"
|
||||
|
||||
# Define output directory
|
||||
OUTPUT_DIR="$SCRIPT_DIR/bin"
|
||||
|
||||
# Define target directory in the user's home
|
||||
TARGET_DIR="$HOME/.azd/bin"
|
||||
|
||||
# Create output and target directories if they don't exist
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
mkdir -p "$TARGET_DIR"
|
||||
|
||||
# List of OS and architecture combinations
|
||||
PLATFORMS=("windows/amd64" "darwin/amd64" "linux/amd64")
|
||||
|
||||
# Loop through platforms and build
|
||||
for PLATFORM in "${PLATFORMS[@]}"; do
|
||||
OS=$(echo $PLATFORM | cut -d'/' -f1)
|
||||
ARCH=$(echo $PLATFORM | cut -d'/' -f2)
|
||||
|
||||
OUTPUT_NAME="$OUTPUT_DIR/$APP_NAME-$OS-$ARCH"
|
||||
TARGET_NAME="$TARGET_DIR/$APP_NAME-$OS-$ARCH"
|
||||
|
||||
if [ "$OS" = "windows" ]; then
|
||||
OUTPUT_NAME+='.exe'
|
||||
TARGET_NAME+='.exe'
|
||||
fi
|
||||
|
||||
echo "Building for $OS/$ARCH..."
|
||||
GOOS=$OS GOARCH=$ARCH go build -o "$OUTPUT_NAME"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "An error occurred while building for $OS/$ARCH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy the build to the target directory
|
||||
cp "$OUTPUT_NAME" "$TARGET_NAME"
|
||||
echo "Copied $OUTPUT_NAME to $TARGET_NAME"
|
||||
done
|
||||
|
||||
echo "Build completed. Binaries are located in the $OUTPUT_DIR directory and copied to $TARGET_DIR."
|
|
@ -0,0 +1,23 @@
|
|||
module github.com/azure/azure-dev/cli/extensions/ai
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/ai/azopenai v0.7.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices v1.6.0
|
||||
github.com/fatih/color v1.17.0
|
||||
github.com/spf13/cobra v1.8.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
golang.org/x/net v0.30.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
)
|
|
@ -0,0 +1,44 @@
|
|||
github.com/Azure/azure-sdk-for-go/sdk/ai/azopenai v0.7.0 h1:rangxMLGrRjqoVMypfkfwGg4ecLtLPlcjEeHnvu1298=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/ai/azopenai v0.7.0/go.mod h1:W+7E7pJtvdzscy/I4tqL5C0/weLsa32wyTbHbPdkkv0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 h1:nyQWyZvwGTvunIMxi1Y9uXkcyr+I7TeNrr/foo4Kpk8=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices v1.6.0 h1:TiYjDq0LCNgtee1teMayYT5FjHmlunWUpthVANUXYPM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices v1.6.0/go.mod h1:yErdzWZBzjNJCnbC1DcUcSVhjTgllT4PyOenFSeXSJI=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@ -0,0 +1,257 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/ai/azopenai"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices"
|
||||
"github.com/azure/azure-dev/cli/extensions/ai/internal/service"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/ext"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/ext/output"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/ux"
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type chatUsageFlags struct {
|
||||
message string
|
||||
systemMessage string
|
||||
subscriptionId string
|
||||
resourceGroup string
|
||||
serviceName string
|
||||
modelName string
|
||||
temperature float32
|
||||
maxTokens int32
|
||||
}
|
||||
|
||||
var (
|
||||
defaultSystemMessage = "You are an AI assistant that helps people find information."
|
||||
defaultTemperature = float32(0.7)
|
||||
defaultMaxTokens = int32(800)
|
||||
)
|
||||
|
||||
func newChatCommand() *cobra.Command {
|
||||
chatFlags := &chatUsageFlags{}
|
||||
|
||||
chatCmd := &cobra.Command{
|
||||
Use: "chat",
|
||||
Short: "Commands for managing chat",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
header := output.CommandHeader{
|
||||
Title: "Chat with AI Model (azd ai chat)",
|
||||
Description: "Start a chat with an AI model from your Azure AI service model deployment.",
|
||||
}
|
||||
header.Print()
|
||||
|
||||
ctx := cmd.Context()
|
||||
azdContext, err := ext.CurrentContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
credential, err := azdContext.Credential()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var aiConfig *service.AiConfig
|
||||
if chatFlags.subscriptionId != "" && chatFlags.resourceGroup != "" && chatFlags.serviceName != "" {
|
||||
aiConfig = &service.AiConfig{
|
||||
Subscription: chatFlags.subscriptionId,
|
||||
ResourceGroup: chatFlags.resourceGroup,
|
||||
Service: chatFlags.serviceName,
|
||||
}
|
||||
} else {
|
||||
aiConfig, err = service.LoadOrPrompt(ctx, azdContext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if chatFlags.modelName != "" {
|
||||
aiConfig.Model = chatFlags.modelName
|
||||
}
|
||||
|
||||
if aiConfig.Model == "" {
|
||||
selectedDeployment, err := service.PromptModelDeployment(ctx, azdContext)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrNoModelDeployments) {
|
||||
return &ext.ErrorWithSuggestion{
|
||||
Err: err,
|
||||
Suggestion: fmt.Sprintf(
|
||||
"Run %s to create a model deployment",
|
||||
color.CyanString("azd ai model deployment create"),
|
||||
),
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
aiConfig.Model = *selectedDeployment.Name
|
||||
if err := service.Save(ctx, azdContext, aiConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
loadingSpinner := ux.NewSpinner(&ux.SpinnerConfig{
|
||||
Text: "Starting chat...",
|
||||
ClearOnStop: true,
|
||||
})
|
||||
|
||||
loadingSpinner.Start(ctx)
|
||||
|
||||
accountClient, err := armcognitiveservices.NewAccountsClient(aiConfig.Subscription, credential, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
account, err := accountClient.Get(ctx, aiConfig.ResourceGroup, aiConfig.Service, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keysResponse, err := accountClient.ListKeys(ctx, aiConfig.ResourceGroup, aiConfig.Service, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keyCredential := azcore.NewKeyCredential(*keysResponse.Key1)
|
||||
|
||||
endpointName := "OpenAI Language Model Instance API"
|
||||
endpoint := *account.Properties.Endpoints[endpointName]
|
||||
chatClient, err := azopenai.NewClientWithKeyCredential(endpoint, keyCredential, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deploymentsClient, err := armcognitiveservices.NewDeploymentsClient(aiConfig.Subscription, credential, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deployment, err := deploymentsClient.Get(ctx, aiConfig.ResourceGroup, aiConfig.Service, aiConfig.Model, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
loadingSpinner.Stop(ctx)
|
||||
|
||||
fmt.Printf(
|
||||
"AI Service: %s %s\n",
|
||||
color.CyanString(aiConfig.Service),
|
||||
color.HiBlackString("(%s)", aiConfig.ResourceGroup),
|
||||
)
|
||||
fmt.Printf(
|
||||
"Model: %s %s\n",
|
||||
color.CyanString(aiConfig.Model),
|
||||
color.HiBlackString(
|
||||
"(Model: %s, Version: %s)",
|
||||
*deployment.Properties.Model.Name,
|
||||
*deployment.Properties.Model.Version,
|
||||
),
|
||||
)
|
||||
fmt.Printf("System Message: %s\n", color.CyanString(chatFlags.systemMessage))
|
||||
fmt.Printf(
|
||||
"Temperature: %s %s\n",
|
||||
color.CyanString(fmt.Sprint(chatFlags.temperature)),
|
||||
color.HiBlackString("(Controls randomness)"),
|
||||
)
|
||||
fmt.Printf(
|
||||
"Max Tokens: %s %s\n",
|
||||
color.CyanString(fmt.Sprint(chatFlags.maxTokens)),
|
||||
color.HiBlackString("(Maximum number of tokens to generate)"),
|
||||
)
|
||||
fmt.Println()
|
||||
|
||||
messages := []azopenai.ChatRequestMessageClassification{}
|
||||
messages = append(messages, &azopenai.ChatRequestSystemMessage{
|
||||
Content: azopenai.NewChatRequestSystemMessageContent(chatFlags.systemMessage),
|
||||
})
|
||||
|
||||
thinkingSpinner := ux.NewSpinner(&ux.SpinnerConfig{
|
||||
Text: "Thinking...",
|
||||
})
|
||||
|
||||
userMessage := chatFlags.message
|
||||
|
||||
for {
|
||||
var err error
|
||||
|
||||
if userMessage == "" {
|
||||
chatPrompt := ux.NewPrompt(&ux.PromptConfig{
|
||||
Message: "User",
|
||||
PlaceHolder: "Press `Ctrl+X` to cancel",
|
||||
Required: true,
|
||||
RequiredMessage: "Please enter a message",
|
||||
ClearOnCompletion: true,
|
||||
IgnoreHintKeys: true,
|
||||
})
|
||||
|
||||
userMessage, err = chatPrompt.Ask()
|
||||
if err != nil {
|
||||
if errors.Is(err, ux.ErrCancelled) {
|
||||
break
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("%s: %s\n", color.GreenString("User"), color.HiBlackString(userMessage))
|
||||
fmt.Println()
|
||||
|
||||
messages = append(messages, &azopenai.ChatRequestUserMessage{
|
||||
Content: azopenai.NewChatRequestUserMessageContent(userMessage),
|
||||
})
|
||||
|
||||
var chatResponse *azopenai.ChatCompletions
|
||||
|
||||
err = thinkingSpinner.Run(ctx, func(ctx context.Context) error {
|
||||
response, err := chatClient.GetChatCompletions(ctx, azopenai.ChatCompletionsOptions{
|
||||
Messages: messages,
|
||||
DeploymentName: &aiConfig.Model,
|
||||
Temperature: &chatFlags.temperature,
|
||||
ResponseFormat: &azopenai.ChatCompletionsTextResponseFormat{},
|
||||
MaxTokens: &chatFlags.maxTokens,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
chatResponse = &response.ChatCompletions
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, choice := range chatResponse.Choices {
|
||||
fmt.Printf("%s: %s\n", color.CyanString("AI"), *choice.Message.Content)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
userMessage = ""
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
chatCmd.Flags().
|
||||
StringVar(&chatFlags.systemMessage, "system-message", defaultSystemMessage, "System message to send to the AI model")
|
||||
chatCmd.Flags().Float32Var(&chatFlags.temperature, "temperature", defaultTemperature, "Temperature for sampling")
|
||||
chatCmd.Flags().Int32Var(&chatFlags.maxTokens, "max-tokens", defaultMaxTokens, "Maximum number of tokens to generate")
|
||||
chatCmd.Flags().StringVarP(&chatFlags.message, "message", "m", "", "Message to send to the AI model")
|
||||
chatCmd.Flags().StringVarP(&chatFlags.modelName, "model deployment name", "d", "", "Name of the model to use")
|
||||
chatCmd.Flags().StringVarP(&chatFlags.resourceGroup, "resource-group", "g", "", "Azure resource group")
|
||||
chatCmd.Flags().StringVarP(&chatFlags.serviceName, "name", "n", "", "Azure AI service name")
|
||||
chatCmd.Flags().StringVarP(&chatFlags.subscriptionId, "subscription", "s", "", "Azure subscription ID")
|
||||
|
||||
return chatCmd
|
||||
}
|
|
@ -0,0 +1,471 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices"
|
||||
"github.com/azure/azure-dev/cli/extensions/ai/internal/service"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/ext"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/ux"
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newDeploymentCommand() *cobra.Command {
|
||||
deploymentCmd := &cobra.Command{
|
||||
Use: "deployment",
|
||||
Short: "Commands for managing Azure AI model deployments",
|
||||
}
|
||||
|
||||
deploymentListCmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all deployments",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
azdContext, err := ext.CurrentContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serviceConfig, err := service.LoadOrPrompt(ctx, azdContext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
credential, err := azdContext.Credential()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deployments := []*armcognitiveservices.Deployment{}
|
||||
|
||||
deploymentsClient, err := armcognitiveservices.NewDeploymentsClient(serviceConfig.Subscription, credential, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deploymentsPager := deploymentsClient.NewListPager(serviceConfig.ResourceGroup, serviceConfig.Service, nil)
|
||||
for deploymentsPager.More() {
|
||||
pageResponse, err := deploymentsPager.NextPage(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deployments = append(deployments, pageResponse.Value...)
|
||||
}
|
||||
|
||||
for _, deployment := range deployments {
|
||||
fmt.Printf("Name: %s\n", *deployment.Name)
|
||||
fmt.Printf("SKU: %s\n", *deployment.SKU.Name)
|
||||
fmt.Printf("Model: %s\n", *deployment.Properties.Model.Name)
|
||||
fmt.Printf("Version: %s\n", *deployment.Properties.Model.Version)
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
deploymentCreateCmd := &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create a new model deployment",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
azdContext, err := ext.CurrentContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serviceConfig, err := service.LoadOrPrompt(ctx, azdContext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
credential, err := azdContext.Credential()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
clientFactory, err := armcognitiveservices.NewClientFactory(serviceConfig.Subscription, credential, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
accountsClient := clientFactory.NewAccountsClient()
|
||||
modelsClient := clientFactory.NewModelsClient()
|
||||
deploymentsClient := clientFactory.NewDeploymentsClient()
|
||||
|
||||
loadingSpinner := ux.NewSpinner(&ux.SpinnerConfig{
|
||||
Text: "Loading AI models",
|
||||
})
|
||||
|
||||
models := []*armcognitiveservices.Model{}
|
||||
|
||||
loadingSpinner.Run(ctx, func(ctx context.Context) error {
|
||||
aiService, err := accountsClient.Get(ctx, serviceConfig.ResourceGroup, serviceConfig.Service, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
modelPager := modelsClient.NewListPager(*aiService.Location, nil)
|
||||
for modelPager.More() {
|
||||
pageResponse, err := modelPager.NextPage(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, model := range pageResponse.Value {
|
||||
if *model.Kind == *aiService.Kind {
|
||||
models = append(models, model)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
modelChoices := make([]string, len(models))
|
||||
for i, model := range models {
|
||||
modelChoices[i] = fmt.Sprintf("%s (Version: %s)", *model.Model.Name, *model.Model.Version)
|
||||
}
|
||||
|
||||
modelSelect := ux.NewSelect(&ux.SelectConfig{
|
||||
Message: "Select a model",
|
||||
Allowed: modelChoices,
|
||||
DisplayCount: 10,
|
||||
DisplayNumbers: to.Ptr(true),
|
||||
})
|
||||
|
||||
selectedModelIndex, err := modelSelect.Ask()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
selectedModel := models[*selectedModelIndex]
|
||||
|
||||
skuChoices := make([]string, len(selectedModel.Model.SKUs))
|
||||
for i, sku := range selectedModel.Model.SKUs {
|
||||
skuChoices[i] = *sku.Name
|
||||
}
|
||||
|
||||
skuPrompt := ux.NewSelect(&ux.SelectConfig{
|
||||
Message: "Select a Deployment Type",
|
||||
Allowed: skuChoices,
|
||||
})
|
||||
|
||||
selectedSkuIndex, err := skuPrompt.Ask()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
selectedSku := selectedModel.Model.SKUs[*selectedSkuIndex]
|
||||
|
||||
var deploymentName string
|
||||
|
||||
namePrompt := ux.NewPrompt(&ux.PromptConfig{
|
||||
Message: "Enter the name for the deployment",
|
||||
DefaultValue: *selectedModel.Model.Name,
|
||||
})
|
||||
|
||||
deploymentName, err = namePrompt.Ask()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deployment := armcognitiveservices.Deployment{
|
||||
Name: &deploymentName,
|
||||
SKU: &armcognitiveservices.SKU{
|
||||
Name: selectedSku.Name,
|
||||
Capacity: selectedSku.Capacity.Default,
|
||||
},
|
||||
Properties: &armcognitiveservices.DeploymentProperties{
|
||||
Model: &armcognitiveservices.DeploymentModel{
|
||||
Format: selectedModel.Model.Format,
|
||||
Name: selectedModel.Model.Name,
|
||||
Version: selectedModel.Model.Version,
|
||||
},
|
||||
RaiPolicyName: to.Ptr("Microsoft.DefaultV2"),
|
||||
VersionUpgradeOption: to.Ptr(
|
||||
armcognitiveservices.DeploymentModelVersionUpgradeOptionOnceNewDefaultVersionAvailable,
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
|
||||
taskList := ux.NewTaskList(&ux.DefaultTaskListConfig)
|
||||
|
||||
if err := taskList.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
taskList.AddTask(fmt.Sprintf("Creating deployment %s", deploymentName), func() (ux.TaskState, error) {
|
||||
existingDeployment, err := deploymentsClient.Get(
|
||||
ctx,
|
||||
serviceConfig.ResourceGroup,
|
||||
serviceConfig.Service,
|
||||
deploymentName,
|
||||
nil,
|
||||
)
|
||||
if err == nil && *existingDeployment.Name == deploymentName {
|
||||
return ux.Error, errors.New("deployment with the same name already exists")
|
||||
}
|
||||
|
||||
poller, err := deploymentsClient.BeginCreateOrUpdate(
|
||||
ctx,
|
||||
serviceConfig.ResourceGroup,
|
||||
serviceConfig.Service,
|
||||
deploymentName,
|
||||
deployment,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return ux.Error, err
|
||||
}
|
||||
|
||||
if _, err := poller.PollUntilDone(ctx, nil); err != nil {
|
||||
return ux.Error, err
|
||||
}
|
||||
|
||||
return ux.Success, nil
|
||||
})
|
||||
|
||||
for {
|
||||
if taskList.Completed() {
|
||||
if err := taskList.Update(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
color.Green("Deployment '%s' created successfully", deploymentName)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
type deploymentDeleteFlags struct {
|
||||
name string
|
||||
force bool
|
||||
}
|
||||
|
||||
deleteFlags := &deploymentDeleteFlags{}
|
||||
|
||||
deploymentDeleteCmd := &cobra.Command{
|
||||
Use: "delete <deployment-name>",
|
||||
Short: "Delete a model deployment",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
azdContext, err := ext.CurrentContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serviceConfig, err := service.LoadOrPrompt(ctx, azdContext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
credential, err := azdContext.Credential()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
clientFactory, err := armcognitiveservices.NewClientFactory(serviceConfig.Subscription, credential, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deploymentsClient := clientFactory.NewDeploymentsClient()
|
||||
|
||||
if deleteFlags.name == "" {
|
||||
deployments := []*armcognitiveservices.Deployment{}
|
||||
|
||||
loadingSpinner := ux.NewSpinner(&ux.SpinnerConfig{
|
||||
Text: "Loading AI deployments",
|
||||
})
|
||||
|
||||
err := loadingSpinner.Run(ctx, func(ctx context.Context) error {
|
||||
deploymentsPager := deploymentsClient.NewListPager(
|
||||
serviceConfig.ResourceGroup,
|
||||
serviceConfig.Service,
|
||||
nil,
|
||||
)
|
||||
for deploymentsPager.More() {
|
||||
pageResponse, err := deploymentsPager.NextPage(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deployments = append(deployments, pageResponse.Value...)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(deployments) == 0 {
|
||||
return fmt.Errorf("no deployments found")
|
||||
}
|
||||
|
||||
deploymentChoices := make([]string, len(deployments))
|
||||
for i, deployment := range deployments {
|
||||
deploymentChoices[i] = fmt.Sprintf(
|
||||
"%s (Model: %s, Version: %s)",
|
||||
*deployment.Name,
|
||||
*deployment.Properties.Model.Name,
|
||||
*deployment.Properties.Model.Version,
|
||||
)
|
||||
}
|
||||
|
||||
deploymentSelect := ux.NewSelect(&ux.SelectConfig{
|
||||
Message: "Select a deployment to delete",
|
||||
Allowed: deploymentChoices,
|
||||
})
|
||||
|
||||
selectedDeploymentIndex, err := deploymentSelect.Ask()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deleteFlags.name = *deployments[*selectedDeploymentIndex].Name
|
||||
}
|
||||
|
||||
_, err = deploymentsClient.Get(ctx, serviceConfig.ResourceGroup, serviceConfig.Service, deleteFlags.name, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deployment '%s' not found", deleteFlags.name)
|
||||
}
|
||||
|
||||
confirmed := to.Ptr(false)
|
||||
|
||||
if !deleteFlags.force {
|
||||
confirmPrompt := ux.NewConfirm(&ux.ConfirmConfig{
|
||||
Message: fmt.Sprintf("Are you sure you want to delete the deployment '%s'?", deleteFlags.name),
|
||||
DefaultValue: to.Ptr(false),
|
||||
})
|
||||
|
||||
confirmed, err = confirmPrompt.Ask()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
|
||||
taskList := ux.NewTaskList(&ux.DefaultTaskListConfig)
|
||||
|
||||
if err := taskList.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
taskList.AddTask(fmt.Sprintf("Deleting deployment %s", deleteFlags.name), func() (ux.TaskState, error) {
|
||||
if !*confirmed {
|
||||
return ux.Skipped, ux.ErrCancelled
|
||||
}
|
||||
|
||||
poller, err := deploymentsClient.BeginDelete(
|
||||
ctx,
|
||||
serviceConfig.ResourceGroup,
|
||||
serviceConfig.Service,
|
||||
deleteFlags.name,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return ux.Error, err
|
||||
}
|
||||
|
||||
if _, err := poller.PollUntilDone(ctx, nil); err != nil {
|
||||
return ux.Error, err
|
||||
}
|
||||
|
||||
return ux.Success, nil
|
||||
})
|
||||
|
||||
for {
|
||||
if taskList.Completed() {
|
||||
if err := taskList.Update(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
color.Green("Deployment '%s' deleted successfully", deleteFlags.name)
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
if err := taskList.Update(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
deploymentDeleteCmd.Flags().StringVarP(&deleteFlags.name, "name", "n", "", "Name of the deployment to delete")
|
||||
deploymentDeleteCmd.Flags().BoolVarP(&deleteFlags.force, "force", "f", false, "Force deletion without confirmation")
|
||||
|
||||
type deploymentSelectFlags struct {
|
||||
deploymentName string
|
||||
}
|
||||
|
||||
selectFlags := &deploymentSelectFlags{}
|
||||
|
||||
deploymentSelectCmd := &cobra.Command{
|
||||
Use: "select",
|
||||
Short: "Select a model",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
azdContext, err := ext.CurrentContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Load AI config
|
||||
aiConfig, err := service.LoadOrPrompt(ctx, azdContext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Select model deployment
|
||||
if selectFlags.deploymentName == "" {
|
||||
selectedDeployment, err := service.PromptModelDeployment(ctx, azdContext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aiConfig.Model = *selectedDeployment.Name
|
||||
} else {
|
||||
aiConfig.Model = selectFlags.deploymentName
|
||||
}
|
||||
|
||||
// Update AI Config
|
||||
if err := service.Save(ctx, azdContext, aiConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
deploymentSelectCmd.Flags().StringVarP(&selectFlags.deploymentName, "name", "n", "", "Model name")
|
||||
|
||||
deploymentCmd.AddCommand(deploymentListCmd)
|
||||
deploymentCmd.AddCommand(deploymentCreateCmd)
|
||||
deploymentCmd.AddCommand(deploymentDeleteCmd)
|
||||
deploymentCmd.AddCommand(deploymentSelectCmd)
|
||||
|
||||
return deploymentCmd
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newModelCommand() *cobra.Command {
|
||||
modelCmd := &cobra.Command{
|
||||
Use: "model",
|
||||
Short: "Commands for managing Azure AI models",
|
||||
}
|
||||
|
||||
modelListCmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all models",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
modelCmd.AddCommand(modelListCmd)
|
||||
modelCmd.AddCommand(newDeploymentCommand())
|
||||
|
||||
return modelCmd
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/ext/debug"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewRootCommand() *cobra.Command {
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "azd ai <group> [options]",
|
||||
Short: "A CLI for managing AI models and services",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
debug.WaitForDebugger()
|
||||
},
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
}
|
||||
|
||||
rootCmd.AddCommand(newModelCommand())
|
||||
rootCmd.AddCommand(newServiceCommand())
|
||||
rootCmd.AddCommand(newChatCommand())
|
||||
|
||||
rootCmd.PersistentFlags().Bool("debug", false, "Enable debug mode")
|
||||
|
||||
return rootCmd
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
|
||||
"github.com/azure/azure-dev/cli/extensions/ai/internal/service"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/ext"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type serviceSetFlags struct {
|
||||
subscription string
|
||||
resourceGroup string
|
||||
serviceName string
|
||||
modelName string
|
||||
}
|
||||
|
||||
func newServiceCommand() *cobra.Command {
|
||||
serviceCmd := &cobra.Command{
|
||||
Use: "service",
|
||||
Short: "Commands for managing Azure AI services",
|
||||
}
|
||||
|
||||
setFlags := &serviceSetFlags{}
|
||||
|
||||
serviceSetCmd := &cobra.Command{
|
||||
Use: "set",
|
||||
Short: "Set the default Azure AI service",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
azdContext, err := ext.CurrentContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var aiConfig *service.AiConfig
|
||||
|
||||
if setFlags.subscription == "" || setFlags.resourceGroup == "" || setFlags.serviceName == "" {
|
||||
selectedAccount, err := service.PromptAccount(ctx, azdContext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parsedResource, err := arm.ParseResourceID(*selectedAccount.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aiConfig = &service.AiConfig{
|
||||
Subscription: parsedResource.SubscriptionID,
|
||||
ResourceGroup: parsedResource.ResourceGroupName,
|
||||
Service: parsedResource.Name,
|
||||
}
|
||||
} else {
|
||||
aiConfig = &service.AiConfig{
|
||||
Subscription: setFlags.subscription,
|
||||
ResourceGroup: setFlags.resourceGroup,
|
||||
Service: setFlags.serviceName,
|
||||
}
|
||||
}
|
||||
|
||||
if err := service.Save(ctx, azdContext, aiConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
serviceSetCmd.Flags().StringVarP(&setFlags.subscription, "subscription", "s", "", "Azure subscription ID")
|
||||
serviceSetCmd.Flags().StringVarP(&setFlags.resourceGroup, "resource-group", "g", "", "Azure resource group")
|
||||
serviceSetCmd.Flags().StringVarP(&setFlags.serviceName, "name", "n", "", "Azure AI service name")
|
||||
|
||||
serviceShowCmd := &cobra.Command{
|
||||
Use: "show",
|
||||
Short: "Show the currently selected Azure AI service",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
azdContext, err := ext.CurrentContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serviceConfig, err := service.Load(ctx, azdContext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Service: %s\n", serviceConfig.Service)
|
||||
fmt.Printf("Resource Group: %s\n", serviceConfig.ResourceGroup)
|
||||
fmt.Printf("Subscription ID: %s\n", serviceConfig.Subscription)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
serviceCmd.AddCommand(serviceSetCmd)
|
||||
serviceCmd.AddCommand(serviceShowCmd)
|
||||
|
||||
return serviceCmd
|
||||
}
|
|
@ -0,0 +1,257 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/azure"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/config"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/ext"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/ext/prompt"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/ux"
|
||||
)
|
||||
|
||||
type AiConfig struct {
|
||||
Subscription string `json:"subscription"`
|
||||
ResourceGroup string `json:"resourceGroup"`
|
||||
Service string `json:"service"`
|
||||
Model string `json:"model"`
|
||||
}
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("service config not found")
|
||||
ErrNoModelDeployments = errors.New("no model deployments found")
|
||||
ErrNoAiServices = errors.New("no Azure AI services found")
|
||||
)
|
||||
|
||||
func LoadOrPrompt(ctx context.Context, azdContext *ext.Context) (*AiConfig, error) {
|
||||
config, err := Load(ctx, azdContext)
|
||||
if err != nil && errors.Is(err, ErrNotFound) {
|
||||
account, err := PromptAccount(ctx, azdContext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parsedResource, err := arm.ParseResourceID(*account.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config = &AiConfig{
|
||||
Subscription: parsedResource.SubscriptionID,
|
||||
ResourceGroup: parsedResource.ResourceGroupName,
|
||||
Service: parsedResource.Name,
|
||||
}
|
||||
|
||||
if err := Save(ctx, azdContext, config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func Load(ctx context.Context, azdContext *ext.Context) (*AiConfig, error) {
|
||||
var azdConfig config.Config
|
||||
|
||||
env, err := azdContext.Environment(ctx)
|
||||
if err == nil {
|
||||
azdConfig = env.Config
|
||||
} else {
|
||||
userConfig, err := azdContext.UserConfig(ctx)
|
||||
if err == nil {
|
||||
azdConfig = userConfig
|
||||
}
|
||||
}
|
||||
|
||||
if azdConfig == nil {
|
||||
return nil, errors.New("azd configuration is not available")
|
||||
}
|
||||
|
||||
var config AiConfig
|
||||
has, err := azdConfig.GetSection("ai.config", &config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if has {
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
func Save(ctx context.Context, azdContext *ext.Context, config *AiConfig) error {
|
||||
if azdContext == nil {
|
||||
return errors.New("azdContext is required")
|
||||
}
|
||||
|
||||
if config == nil {
|
||||
return errors.New("config is required")
|
||||
}
|
||||
|
||||
credential, err := azdContext.Credential()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
clientFactory, err := armcognitiveservices.NewClientFactory(config.Subscription, credential, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
accountClient := clientFactory.NewAccountsClient()
|
||||
|
||||
// Validate account name
|
||||
_, err = accountClient.Get(ctx, config.ResourceGroup, config.Service, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("the specified service configuration is invalid: %w", err)
|
||||
}
|
||||
|
||||
// Validate model deployment name
|
||||
if config.Model != "" {
|
||||
deploymentClient := clientFactory.NewDeploymentsClient()
|
||||
_, err = deploymentClient.Get(ctx, config.ResourceGroup, config.Service, config.Model, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("the specified model deployment is invalid: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
env, err := azdContext.Environment(ctx)
|
||||
if err == nil && env != nil {
|
||||
if err := env.Config.Set("ai.config", config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := azdContext.SaveEnvironment(ctx, env); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
userConfig, err := azdContext.UserConfig(ctx)
|
||||
if err == nil && userConfig != nil {
|
||||
if err := userConfig.Set("ai.config", config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := azdContext.SaveUserConfig(ctx, userConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("unable to save service configuration")
|
||||
}
|
||||
|
||||
func PromptAccount(ctx context.Context, azdContext *ext.Context) (*armcognitiveservices.Account, error) {
|
||||
subscription, err := prompt.PromptSubscription(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
credential, err := azdContext.Credential()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
selectedAccount, err := prompt.PromptSubscriptionResource(ctx, subscription, prompt.PromptResourceOptions{
|
||||
ResourceType: to.Ptr(azure.ResourceTypeCognitiveServiceAccount),
|
||||
ResourceTypeDisplayName: "Azure AI service",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parsedService, err := arm.ParseResourceID(selectedAccount.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accountClient, err := armcognitiveservices.NewAccountsClient(subscription.Id, credential, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accountResponse, err := accountClient.Get(ctx, parsedService.ResourceGroupName, parsedService.Name, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &accountResponse.Account, nil
|
||||
}
|
||||
|
||||
func PromptModelDeployment(ctx context.Context, azdContext *ext.Context) (*armcognitiveservices.Deployment, error) {
|
||||
aiConfig, err := LoadOrPrompt(ctx, azdContext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
credential, err := azdContext.Credential()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deploymentsClient, err := armcognitiveservices.NewDeploymentsClient(aiConfig.Subscription, credential, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deploymentList := []*armcognitiveservices.Deployment{}
|
||||
|
||||
loadingSpinner := ux.NewSpinner(&ux.SpinnerConfig{
|
||||
Text: "Loading model deployments...",
|
||||
})
|
||||
|
||||
err = loadingSpinner.Run(ctx, func(ctx context.Context) error {
|
||||
modelPager := deploymentsClient.NewListPager(aiConfig.ResourceGroup, aiConfig.Service, nil)
|
||||
for modelPager.More() {
|
||||
models, err := modelPager.NextPage(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deploymentList = append(deploymentList, models.Value...)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
choices := make([]string, len(deploymentList))
|
||||
for i, deployment := range deploymentList {
|
||||
choices[i] = fmt.Sprintf(
|
||||
"%s (Model: %s, Version: %s)",
|
||||
*deployment.Name,
|
||||
*deployment.Properties.Model.Name,
|
||||
*deployment.Properties.Model.Version,
|
||||
)
|
||||
}
|
||||
|
||||
if len(choices) == 0 {
|
||||
return nil, ErrNoModelDeployments
|
||||
}
|
||||
|
||||
modelSelector := ux.NewSelect(&ux.SelectConfig{
|
||||
Message: "Select a model",
|
||||
Allowed: choices,
|
||||
DisplayCount: 10,
|
||||
DisplayNumbers: ux.Ptr(true),
|
||||
})
|
||||
|
||||
selectedModelIndex, err := modelSelector.Ask()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return deploymentList[*selectedModelIndex], nil
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/azure/azure-dev/cli/extensions/ai/internal/cmd"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/ext"
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Execute the root command
|
||||
ctx := context.Background()
|
||||
rootCmd := cmd.NewRootCommand()
|
||||
|
||||
if err := rootCmd.ExecuteContext(ctx); err != nil {
|
||||
var errWithSuggestion *ext.ErrorWithSuggestion
|
||||
|
||||
if ok := errors.As(err, &errWithSuggestion); ok {
|
||||
color.Red("Error: %v", errWithSuggestion.Err)
|
||||
fmt.Printf("%s: %s\n", color.YellowString("Suggestion:"), errWithSuggestion.Suggestion)
|
||||
} else {
|
||||
color.Red("Error: %v", err)
|
||||
}
|
||||
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# env file
|
||||
.env
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024 Wallace Breza
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,3 @@
|
|||
# azd test extension
|
||||
|
||||
A test AZD extension
|
|
@ -0,0 +1,40 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Get the directory of the script
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
# Change to the script directory
|
||||
cd "$SCRIPT_DIR" || exit
|
||||
|
||||
# Define application name
|
||||
APP_NAME="azd-ext-test"
|
||||
|
||||
# Define output directory
|
||||
OUTPUT_DIR="$SCRIPT_DIR/bin"
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# List of OS and architecture combinations
|
||||
PLATFORMS=("windows/amd64" "darwin/amd64" "linux/amd64")
|
||||
|
||||
# Loop through platforms and build
|
||||
for PLATFORM in "${PLATFORMS[@]}"; do
|
||||
OS=$(echo $PLATFORM | cut -d'/' -f1)
|
||||
ARCH=$(echo $PLATFORM | cut -d'/' -f2)
|
||||
|
||||
OUTPUT_NAME="$OUTPUT_DIR/$APP_NAME-$OS-$ARCH"
|
||||
if [ "$OS" = "windows" ]; then
|
||||
OUTPUT_NAME+='.exe'
|
||||
fi
|
||||
|
||||
echo "Building for $OS/$ARCH..."
|
||||
GOOS=$OS GOARCH=$ARCH go build -o "$OUTPUT_NAME"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "An error occurred while building for $OS/$ARCH"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Build completed. Binaries are located in the $OUTPUT_DIR directory."
|
|
@ -0,0 +1,16 @@
|
|||
module github.com/azure/azure-dev/cli/extensions/test
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/fatih/color v1.17.0
|
||||
github.com/spf13/cobra v1.8.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
)
|
|
@ -0,0 +1,20 @@
|
|||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@ -0,0 +1,48 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type rootFlags struct {
|
||||
unit bool
|
||||
}
|
||||
|
||||
func main() {
|
||||
flags := rootFlags{}
|
||||
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "azd test [options]",
|
||||
Short: "A tool to help test azd projects and services.",
|
||||
Example: "azd test <service> --unit",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
fmt.Print("Running tests for all services")
|
||||
} else {
|
||||
fmt.Printf("Running tests for %s service", args[0])
|
||||
}
|
||||
|
||||
if flags.unit {
|
||||
fmt.Print(" (including unit tests)")
|
||||
}
|
||||
|
||||
fmt.Print("\n")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
rootCmd.Flags().BoolVar(&flags.unit, "unit", false, "Runs unit tests")
|
||||
|
||||
ctx := context.Background()
|
||||
if err := rootCmd.ExecuteContext(ctx); err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
go 1.23.0
|
||||
|
||||
use (
|
||||
./azd
|
||||
./extensions/ai
|
||||
./extensions/test
|
||||
./sdk/azdcore
|
||||
)
|
|
@ -0,0 +1,58 @@
|
|||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0/go.mod h1:uReU2sSxZExRPBAg3qKzmAucSi51+SP1OhohieR821Q=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0/go.mod h1:1fXstnBMas5kzG+S3q8UoJcmyU6nUeunJcMDHcRYHhs=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.0/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.0/go.mod h1:WCPBHsOXfBVnivScjs2ypRfimjEW0qPVLGgJkZlrIOA=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
|
||||
github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a/go.mod h1:YPNKjjE7Ubp9dTbnWvsP3HT+hYnY6TfXzubYTBeUxc8=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
|
||||
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
|
@ -0,0 +1,115 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// cspell: disable
|
||||
|
||||
// jwtClaimsRegex is a regular expression for JWT. A JWT is a string with three base64 encoded
|
||||
// components (using the "url safe" base64 alphabet) separated by dots. For example:
|
||||
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
|
||||
//
|
||||
//nolint:lll
|
||||
var jwtClaimsRegex = regexp.MustCompile(`^[a-zA-Z0-9-_]*\.([a-zA-Z0-9-_]*)\.[a-zA-Z0-9-_]*$`)
|
||||
|
||||
// cspell: enable
|
||||
|
||||
// TokenClaims contains claims about a user from an access token.
|
||||
// https://learn.microsoft.com/en-us/entra/identity-platform/id-token-claims-reference.
|
||||
type TokenClaims struct {
|
||||
PreferredUsername string `json:"preferred_username,omitempty"`
|
||||
UniqueName string `json:"unique_name,omitempty"`
|
||||
GivenName string `json:"given_name,omitempty"`
|
||||
FamilyName string `json:"family_name,omitempty"`
|
||||
MiddleName string `json:"middle_name,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Oid string `json:"oid,omitempty"`
|
||||
TenantId string `json:"tid,omitempty"`
|
||||
Subject string `json:"sub,omitempty"`
|
||||
Upn string `json:"upn,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
AlternativeId string `json:"alternative_id,omitempty"`
|
||||
Issuer string `json:"iss,omitempty"`
|
||||
Audience string `json:"aud,omitempty"`
|
||||
ExpirationTime int64 `json:"exp,omitempty"`
|
||||
IssuedAt int64 `json:"iat,omitempty"`
|
||||
NotBefore int64 `json:"nbf,omitempty"`
|
||||
}
|
||||
|
||||
// Returns an ID associated with the account.
|
||||
// This ID is suitable for local use, and not for any server authorization use.
|
||||
func (tc *TokenClaims) LocalAccountId() string {
|
||||
if tc.Oid != "" {
|
||||
return tc.Oid
|
||||
}
|
||||
|
||||
// Fall back to sub if oid is not present.
|
||||
// This happens, for example, for personal accounts in their home tenant.
|
||||
return tc.Subject
|
||||
}
|
||||
|
||||
// Returns a display name for the account.
|
||||
func (tc *TokenClaims) DisplayUsername() string {
|
||||
// For v2.0 token, use preferred_username
|
||||
if tc.PreferredUsername != "" {
|
||||
return tc.PreferredUsername
|
||||
}
|
||||
|
||||
// Fallback to unique_name for v1.0 token
|
||||
return tc.UniqueName
|
||||
}
|
||||
|
||||
func GetTenantIdFromToken(token string) (string, error) {
|
||||
claims, err := GetClaimsFromAccessToken(token)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if claims.TenantId == "" {
|
||||
return "", errors.New("no tid claim")
|
||||
}
|
||||
|
||||
return claims.TenantId, nil
|
||||
}
|
||||
|
||||
// GetOidFromAccessToken extracts a string claim with the name "oid" from an access token.
|
||||
// Access Tokens are JWT and the middle component is a base64 encoded string of a JSON object
|
||||
// with claims.
|
||||
func GetOidFromAccessToken(token string) (string, error) {
|
||||
claims, err := GetClaimsFromAccessToken(token)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if claims.Oid == "" {
|
||||
return "", errors.New("no oid claim")
|
||||
}
|
||||
|
||||
return claims.Oid, nil
|
||||
}
|
||||
|
||||
// GetClaimsFromAccessToken extracts claims from an access token.
|
||||
// Access Tokens are JWT and the middle component is a base64 encoded string of a JSON object
|
||||
// with claims.
|
||||
func GetClaimsFromAccessToken(token string) (TokenClaims, error) {
|
||||
matches := jwtClaimsRegex.FindStringSubmatch(token)
|
||||
if len(matches) != 2 {
|
||||
return TokenClaims{}, errors.New("malformed access token")
|
||||
}
|
||||
|
||||
bytes, err := base64.RawURLEncoding.DecodeString(matches[1])
|
||||
if err != nil {
|
||||
return TokenClaims{}, err
|
||||
}
|
||||
|
||||
var claims TokenClaims
|
||||
if err := json.Unmarshal(bytes, &claims); err != nil {
|
||||
return TokenClaims{}, err
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
|
||||
)
|
||||
|
||||
const (
|
||||
ConfigPath = "cloud"
|
||||
|
||||
AzurePublicName = "AzureCloud"
|
||||
AzureChinaCloudName = "AzureChinaCloud"
|
||||
AzureUSGovernmentName = "AzureUSGovernment"
|
||||
)
|
||||
|
||||
type Cloud struct {
|
||||
Configuration cloud.Configuration
|
||||
|
||||
// The base URL for the cloud's portal (e.g. https://portal.azure.com for
|
||||
// Azure public cloud).
|
||||
PortalUrlBase string
|
||||
|
||||
// The suffix for the cloud's storage endpoints (e.g. core.windows.net for
|
||||
// Azure public cloud). These are well known values and can be found at:
|
||||
// https://<management-endpoint>/metadata/endpoints?api-version=2023-12-01
|
||||
StorageEndpointSuffix string
|
||||
|
||||
// The suffix for the cloud's container registry endpoints. These are well
|
||||
// known values and can be found at:
|
||||
// https://<management-endpoint>/metadata/endpoints?api-version=2023-12-01
|
||||
ContainerRegistryEndpointSuffix string
|
||||
}
|
||||
|
||||
type CloudConfig struct {
|
||||
Name string `json:"name,omitempty" yaml:"name,omitempty"`
|
||||
}
|
||||
|
||||
func NewCloud(config *CloudConfig) (*Cloud, error) {
|
||||
if cloud, err := parseCloudName(config.Name); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return cloud, nil
|
||||
}
|
||||
}
|
||||
|
||||
func ParseCloudConfig(partialConfig any) (*CloudConfig, error) {
|
||||
var config *CloudConfig
|
||||
|
||||
jsonBytes, err := json.Marshal(partialConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal cloud configuration: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(jsonBytes, &config); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal cloud configuration: %w", err)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func AzurePublic() *Cloud {
|
||||
return &Cloud{
|
||||
Configuration: cloud.AzurePublic,
|
||||
PortalUrlBase: "https://portal.azure.com",
|
||||
StorageEndpointSuffix: "core.windows.net",
|
||||
ContainerRegistryEndpointSuffix: "azurecr.io",
|
||||
}
|
||||
}
|
||||
|
||||
func AzureGovernment() *Cloud {
|
||||
return &Cloud{
|
||||
Configuration: cloud.AzureGovernment,
|
||||
PortalUrlBase: "https://portal.azure.us",
|
||||
StorageEndpointSuffix: "core.usgovcloudapi.net",
|
||||
ContainerRegistryEndpointSuffix: "azurecr.us",
|
||||
}
|
||||
}
|
||||
|
||||
func AzureChina() *Cloud {
|
||||
return &Cloud{
|
||||
Configuration: cloud.AzureChina,
|
||||
PortalUrlBase: "https://portal.azure.cn",
|
||||
StorageEndpointSuffix: "core.chinacloudapi.cn",
|
||||
ContainerRegistryEndpointSuffix: "azurecr.cn",
|
||||
}
|
||||
}
|
||||
|
||||
func parseCloudName(name string) (*Cloud, error) {
|
||||
if name == AzurePublicName || name == "" {
|
||||
return AzurePublic(), nil
|
||||
} else if name == AzureChinaCloudName {
|
||||
return AzureChina(), nil
|
||||
} else if name == AzureUSGovernmentName {
|
||||
return AzureGovernment(), nil
|
||||
}
|
||||
|
||||
return &Cloud{}, fmt.Errorf("Cloud name '%s' not found.", name)
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package azure
|
||||
|
||||
type ResourceType string
|
||||
|
||||
const (
|
||||
ResourceTypeApim ResourceType = "Microsoft.ApiManagement/service"
|
||||
ResourceTypeAppConfig ResourceType = "Microsoft.AppConfiguration/configurationStores"
|
||||
ResourceTypeAppInsightComponent ResourceType = "Microsoft.Insights/components"
|
||||
ResourceTypeCacheForRedis ResourceType = "Microsoft.Cache/redis"
|
||||
ResourceTypeCDNProfile ResourceType = "Microsoft.Cdn/profiles"
|
||||
ResourceTypeCosmosDb ResourceType = "Microsoft.DocumentDB/databaseAccounts"
|
||||
ResourceTypeContainerApp ResourceType = "Microsoft.App/containerApps"
|
||||
ResourceTypeSpringApp ResourceType = "Microsoft.AppPlatform/Spring"
|
||||
ResourceTypeContainerAppEnvironment ResourceType = "Microsoft.App/managedEnvironments"
|
||||
ResourceTypeDeployment ResourceType = "Microsoft.Resources/deployments"
|
||||
ResourceTypeKeyVault ResourceType = "Microsoft.KeyVault/vaults"
|
||||
ResourceTypeManagedHSM ResourceType = "Microsoft.KeyVault/managedHSMs"
|
||||
ResourceTypeLoadTest ResourceType = "Microsoft.LoadTestService/loadTests"
|
||||
ResourceTypeLogAnalyticsWorkspace ResourceType = "Microsoft.OperationalInsights/workspaces"
|
||||
ResourceTypePortalDashboard ResourceType = "Microsoft.Portal/dashboards"
|
||||
ResourceTypePostgreSqlServer ResourceType = "Microsoft.DBforPostgreSQL/flexibleServers"
|
||||
ResourceTypeMySqlServer ResourceType = "Microsoft.DBforMySQL/flexibleServers"
|
||||
ResourceTypeResourceGroup ResourceType = "Microsoft.Resources/resourceGroups"
|
||||
ResourceTypeStorageAccount ResourceType = "Microsoft.Storage/storageAccounts"
|
||||
ResourceTypeStaticWebSite ResourceType = "Microsoft.Web/staticSites"
|
||||
ResourceTypeServiceBusNamespace ResourceType = "Microsoft.ServiceBus/namespaces"
|
||||
ResourceTypeServicePlan ResourceType = "Microsoft.Web/serverfarms"
|
||||
ResourceTypeSqlServer ResourceType = "Microsoft.Sql/servers"
|
||||
ResourceTypeVirtualNetwork ResourceType = "Microsoft.Network/virtualNetworks"
|
||||
ResourceTypeWebSite ResourceType = "Microsoft.Web/sites"
|
||||
ResourceTypeContainerRegistry ResourceType = "Microsoft.ContainerRegistry/registries"
|
||||
ResourceTypeManagedCluster ResourceType = "Microsoft.ContainerService/managedClusters"
|
||||
ResourceTypeAgentPool ResourceType = "Microsoft.ContainerService/managedClusters/agentPools"
|
||||
ResourceTypeCognitiveServiceAccount ResourceType = "Microsoft.CognitiveServices/accounts"
|
||||
ResourceTypeSearchService ResourceType = "Microsoft.Search/searchServices"
|
||||
ResourceTypeVideoIndexer ResourceType = "Microsoft.VideoIndexer/accounts"
|
||||
ResourceTypePrivateEndpoint ResourceType = "Microsoft.Network/privateEndpoints"
|
||||
ResourceTypeDevCenter ResourceType = "Microsoft.DevCenter/devcenters"
|
||||
ResourceTypeDevCenterProject ResourceType = "Microsoft.DevCenter/projects"
|
||||
ResourceTypeMachineLearningWorkspace ResourceType = "Microsoft.MachineLearningServices/workspaces"
|
||||
ResourceTypeMachineLearningConnection ResourceType = "Microsoft.MachineLearningServices/workspaces/connections"
|
||||
|
||||
//nolint:lll
|
||||
ResourceTypeMachineLearningEndpoint ResourceType = "Microsoft.MachineLearningServices/workspaces/onlineEndpoints"
|
||||
ResourceTypeCognitiveServiceAccountDeployment ResourceType = "Microsoft.CognitiveServices/accounts/deployments"
|
||||
)
|
||||
|
||||
type Subscription struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
TenantId string `json:"tenantId"`
|
||||
// The tenant under which the user has access to the subscription.
|
||||
UserAccessTenantId string `json:"userAccessTenantId"`
|
||||
IsDefault bool `json:"isDefault,omitempty"`
|
||||
}
|
||||
|
||||
type Location struct {
|
||||
// The name of the location (e.g. "westus2")
|
||||
Name string `json:"name"`
|
||||
// The human friendly name of the location (e.g. "West US 2")
|
||||
DisplayName string `json:"displayName"`
|
||||
// The human friendly name of the location, prefixed with a
|
||||
// region name (e.g "(US) West US 2")
|
||||
RegionalDisplayName string `json:"regionalDisplayName"`
|
||||
}
|
||||
|
||||
type ResourceGroup struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Location string `json:"location"`
|
||||
}
|
||||
|
||||
type Resource struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Location string `json:"location"`
|
||||
}
|
||||
|
||||
type ResourceExtended struct {
|
||||
Resource
|
||||
Kind string `json:"kind"`
|
||||
}
|
|
@ -0,0 +1,251 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
|
||||
)
|
||||
|
||||
// Optional parameters for resource group listing.
|
||||
type ListResourceGroupOptions struct {
|
||||
// An optional tag filter
|
||||
TagFilter *Filter
|
||||
// An optional filter expression to filter the resource group results
|
||||
// https://learn.microsoft.com/en-us/rest/api/resources/resource-groups/list
|
||||
Filter *string
|
||||
}
|
||||
|
||||
type Filter struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
// Optional parameters for resource group resources listing.
|
||||
type ListResourceGroupResourcesOptions struct {
|
||||
// An optional filter expression to filter the resource list result
|
||||
// https://learn.microsoft.com/en-us/rest/api/resources/resources/list-by-resource-group#uri-parameters
|
||||
Filter *string
|
||||
}
|
||||
|
||||
type ResourceService struct {
|
||||
credential azcore.TokenCredential
|
||||
armClientOptions *arm.ClientOptions
|
||||
}
|
||||
|
||||
func NewResourceService(
|
||||
credential azcore.TokenCredential,
|
||||
armClientOptions *arm.ClientOptions,
|
||||
) *ResourceService {
|
||||
return &ResourceService{
|
||||
credential: credential,
|
||||
armClientOptions: armClientOptions,
|
||||
}
|
||||
}
|
||||
|
||||
func (rs *ResourceService) GetResource(
|
||||
ctx context.Context, subscriptionId string, resourceId string, apiVersion string) (ResourceExtended, error) {
|
||||
client, err := rs.createResourcesClient(subscriptionId)
|
||||
if err != nil {
|
||||
return ResourceExtended{}, err
|
||||
}
|
||||
|
||||
res, err := client.GetByID(ctx, resourceId, apiVersion, nil)
|
||||
if err != nil {
|
||||
return ResourceExtended{}, fmt.Errorf("getting resource by id: %w", err)
|
||||
}
|
||||
|
||||
return ResourceExtended{
|
||||
Resource: Resource{
|
||||
Id: *res.ID,
|
||||
Name: *res.Name,
|
||||
Type: *res.Type,
|
||||
Location: *res.Location,
|
||||
},
|
||||
Kind: *res.Kind,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (rs *ResourceService) ListResourceGroupResources(
|
||||
ctx context.Context,
|
||||
subscriptionId string,
|
||||
resourceGroupName string,
|
||||
listOptions *ListResourceGroupResourcesOptions,
|
||||
) ([]*Resource, error) {
|
||||
client, err := rs.createResourcesClient(subscriptionId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Filter expression on the underlying REST API are different from --query param in az cli.
|
||||
// https://learn.microsoft.com/en-us/rest/api/resources/resources/list-by-resource-group#uri-parameters
|
||||
options := armresources.ClientListByResourceGroupOptions{}
|
||||
if listOptions != nil && *listOptions.Filter != "" {
|
||||
options.Filter = listOptions.Filter
|
||||
}
|
||||
|
||||
resources := []*Resource{}
|
||||
pager := client.NewListByResourceGroupPager(resourceGroupName, &options)
|
||||
for pager.More() {
|
||||
page, err := pager.NextPage(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, resource := range page.ResourceListResult.Value {
|
||||
resources = append(resources, &Resource{
|
||||
Id: *resource.ID,
|
||||
Name: *resource.Name,
|
||||
Type: *resource.Type,
|
||||
Location: *resource.Location,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
func (rs *ResourceService) ListResourceGroup(
|
||||
ctx context.Context,
|
||||
subscriptionId string,
|
||||
listOptions *ListResourceGroupOptions,
|
||||
) ([]*Resource, error) {
|
||||
client, err := rs.createResourceGroupClient(subscriptionId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Filter values differ from those support in the --query param of az cli.
|
||||
// https://learn.microsoft.com/en-us/rest/api/resources/resource-groups/list
|
||||
options := armresources.ResourceGroupsClientListOptions{}
|
||||
if listOptions != nil {
|
||||
if listOptions.TagFilter != nil {
|
||||
tagFilter := fmt.Sprintf(
|
||||
"tagName eq '%s' and tagValue eq '%s'",
|
||||
listOptions.TagFilter.Key,
|
||||
listOptions.TagFilter.Value,
|
||||
)
|
||||
options.Filter = &tagFilter
|
||||
} else if listOptions.Filter != nil {
|
||||
options.Filter = listOptions.Filter
|
||||
}
|
||||
}
|
||||
|
||||
groups := []*Resource{}
|
||||
pager := client.NewListPager(&options)
|
||||
|
||||
for pager.More() {
|
||||
page, err := pager.NextPage(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, group := range page.ResourceGroupListResult.Value {
|
||||
groups = append(groups, &Resource{
|
||||
Id: *group.ID,
|
||||
Name: *group.Name,
|
||||
Type: *group.Type,
|
||||
Location: *group.Location,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func (rs *ResourceService) ListSubscriptionResources(
|
||||
ctx context.Context,
|
||||
subscriptionId string,
|
||||
listOptions *armresources.ClientListOptions,
|
||||
) ([]*Resource, error) {
|
||||
client, err := rs.createResourcesClient(subscriptionId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Filter expression on the underlying REST API are different from --query param in az cli.
|
||||
// https://learn.microsoft.com/en-us/rest/api/resources/resources/list-by-resource-group#uri-parameters
|
||||
options := armresources.ClientListOptions{}
|
||||
if listOptions != nil && *listOptions.Filter != "" {
|
||||
options.Filter = listOptions.Filter
|
||||
}
|
||||
|
||||
resources := []*Resource{}
|
||||
pager := client.NewListPager(&options)
|
||||
for pager.More() {
|
||||
page, err := pager.NextPage(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, resource := range page.ResourceListResult.Value {
|
||||
resources = append(resources, &Resource{
|
||||
Id: *resource.ID,
|
||||
Name: *resource.Name,
|
||||
Type: *resource.Type,
|
||||
Location: *resource.Location,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
func (rs *ResourceService) CreateOrUpdateResourceGroup(
|
||||
ctx context.Context,
|
||||
subscriptionId string,
|
||||
resourceGroupName string,
|
||||
location string,
|
||||
tags map[string]*string,
|
||||
) error {
|
||||
client, err := rs.createResourceGroupClient(subscriptionId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = client.CreateOrUpdate(ctx, resourceGroupName, armresources.ResourceGroup{
|
||||
Location: &location,
|
||||
Tags: tags,
|
||||
}, nil)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (rs *ResourceService) DeleteResourceGroup(ctx context.Context, subscriptionId string, resourceGroupName string) error {
|
||||
client, err := rs.createResourceGroupClient(subscriptionId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
poller, err := client.BeginDelete(ctx, resourceGroupName, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("beginning resource group deletion: %w", err)
|
||||
}
|
||||
|
||||
_, err = poller.PollUntilDone(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting resource group: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rs *ResourceService) createResourcesClient(subscriptionId string) (*armresources.Client, error) {
|
||||
client, err := armresources.NewClient(subscriptionId, rs.credential, rs.armClientOptions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating Resource client: %w", err)
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (rs *ResourceService) createResourceGroupClient(subscriptionId string) (*armresources.ResourceGroupsClient, error) {
|
||||
client, err := armresources.NewResourceGroupsClient(subscriptionId, rs.credential, rs.armClientOptions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating ResourceGroup client: %w", err)
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/azure"
|
||||
)
|
||||
|
||||
// AccountConfig contains the configuration for connecting to a storage account
|
||||
type AccountConfig struct {
|
||||
AccountName string
|
||||
ContainerName string
|
||||
Endpoint string
|
||||
}
|
||||
|
||||
var (
|
||||
ErrContainerNotFound = errors.New("container not found")
|
||||
)
|
||||
|
||||
type BlobClient interface {
|
||||
// Download downloads a blob from the configured storage account container.
|
||||
Download(ctx context.Context, blobPath string) (io.ReadCloser, error)
|
||||
|
||||
// Upload uploads a blob to the configured storage account container.
|
||||
Upload(ctx context.Context, blobPath string, reader io.Reader) error
|
||||
|
||||
// Delete deletes a blob from the configured storage account container.
|
||||
Delete(ctx context.Context, blobPath string) error
|
||||
|
||||
// Items returns a list of blobs in the configured storage account container.
|
||||
Items(ctx context.Context) ([]*Blob, error)
|
||||
}
|
||||
|
||||
// NewBlobClient creates a new BlobClient instance to manage blobs within a container.
|
||||
func NewBlobClient(
|
||||
config *AccountConfig,
|
||||
client *azblob.Client,
|
||||
) BlobClient {
|
||||
return &blobClient{
|
||||
config: config,
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
type blobClient struct {
|
||||
config *AccountConfig
|
||||
client *azblob.Client
|
||||
}
|
||||
|
||||
// Blob represents a blob within a storage account container.
|
||||
type Blob struct {
|
||||
Name string
|
||||
Path string
|
||||
CreationTime time.Time
|
||||
LastModified time.Time
|
||||
}
|
||||
|
||||
// Items returns a list of blobs in the configured storage account container.
|
||||
func (bc *blobClient) Items(ctx context.Context) ([]*Blob, error) {
|
||||
if err := bc.ensureContainerExists(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blobs := []*Blob{}
|
||||
|
||||
pager := bc.client.NewListBlobsFlatPager(bc.config.ContainerName, nil)
|
||||
for pager.More() {
|
||||
page, err := pager.NextPage(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get next page of blobs, %w", err)
|
||||
}
|
||||
|
||||
for _, blob := range page.Segment.BlobItems {
|
||||
blobs = append(blobs, &Blob{
|
||||
Name: filepath.Base(*blob.Name),
|
||||
Path: *blob.Name,
|
||||
CreationTime: *blob.Properties.CreationTime,
|
||||
LastModified: *blob.Properties.LastModified,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return blobs, nil
|
||||
}
|
||||
|
||||
// Download downloads a blob from the configured storage account container.
|
||||
func (bc *blobClient) Download(ctx context.Context, blobPath string) (io.ReadCloser, error) {
|
||||
if err := bc.ensureContainerExists(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := bc.client.DownloadStream(ctx, bc.config.ContainerName, blobPath, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to download blob '%s', %w", blobPath, err)
|
||||
}
|
||||
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
// Upload uploads a blob to the configured storage account container.
|
||||
func (bc *blobClient) Upload(ctx context.Context, blobPath string, reader io.Reader) error {
|
||||
if err := bc.ensureContainerExists(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := bc.client.UploadStream(ctx, bc.config.ContainerName, blobPath, reader, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to upload blob '%s', %w", blobPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete deletes a blob from the configured storage account container.
|
||||
func (bc *blobClient) Delete(ctx context.Context, blobPath string) error {
|
||||
if err := bc.ensureContainerExists(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := bc.client.DeleteBlob(ctx, bc.config.ContainerName, blobPath, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete blob '%s', %w", blobPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if the specified container exists
|
||||
// If it doesn't already exist then create it
|
||||
func (bc *blobClient) ensureContainerExists(ctx context.Context) error {
|
||||
exists := false
|
||||
|
||||
pager := bc.client.NewListContainersPager(nil)
|
||||
for pager.More() {
|
||||
page, err := pager.NextPage(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed getting next page of containers: %w", err)
|
||||
}
|
||||
|
||||
for _, container := range page.ContainerItems {
|
||||
if *container.Name == bc.config.ContainerName {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !exists {
|
||||
_, err := bc.client.CreateContainer(ctx, bc.config.ContainerName, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create container '%s', %w", bc.config.ContainerName, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createClient creates a new blob client and caches it for future use
|
||||
func NewBlobSdkClient(
|
||||
credential azcore.TokenCredential,
|
||||
accountConfig *AccountConfig,
|
||||
coreClientOptions *azcore.ClientOptions,
|
||||
cloud *azure.Cloud,
|
||||
) (*azblob.Client, error) {
|
||||
blobOptions := &azblob.ClientOptions{
|
||||
ClientOptions: *coreClientOptions,
|
||||
}
|
||||
|
||||
if accountConfig.Endpoint == "" {
|
||||
accountConfig.Endpoint = cloud.StorageEndpointSuffix
|
||||
}
|
||||
|
||||
serviceUrl := fmt.Sprintf("https://%s.blob.%s", accountConfig.AccountName, accountConfig.Endpoint)
|
||||
client, err := azblob.NewClient(serviceUrl, credential, blobOptions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create blob client, %w", err)
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azfile/service"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azfile/share"
|
||||
)
|
||||
|
||||
type FileShareService interface {
|
||||
// Upload files from source path to a file share
|
||||
UploadPath(ctx context.Context, subId, shareUrl, source string) error
|
||||
}
|
||||
|
||||
func NewFileShareService(
|
||||
credential azcore.TokenCredential,
|
||||
options *arm.ClientOptions,
|
||||
) FileShareService {
|
||||
return &fileShareClient{
|
||||
credential: credential,
|
||||
options: options,
|
||||
}
|
||||
}
|
||||
|
||||
type fileShareClient struct {
|
||||
credential azcore.TokenCredential
|
||||
options *arm.ClientOptions
|
||||
}
|
||||
|
||||
func (f *fileShareClient) UploadPath(ctx context.Context, subId, shareUrl, source string) error {
|
||||
return filepath.WalkDir(source, func(path string, info fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
destination := strings.TrimPrefix(path, source+string(filepath.Separator))
|
||||
if err := f.uploadFile(ctx, shareUrl, path, destination, f.credential); err != nil {
|
||||
return fmt.Errorf("error uploading file to file share: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// uploadFile implements FileShareService.
|
||||
func (f *fileShareClient) uploadFile(
|
||||
ctx context.Context, fileShareUrl, source, dest string, credential azcore.TokenCredential) error {
|
||||
|
||||
client, err := share.NewClient(fileShareUrl, credential, &share.ClientOptions{
|
||||
ClientOptions: f.options.ClientOptions,
|
||||
FileRequestIntent: to.Ptr(service.ShareTokenIntentBackup),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dirClient := client.NewRootDirectoryClient()
|
||||
dirPaths := strings.Split(dest, string(os.PathSeparator))
|
||||
incrementPath := ""
|
||||
for _, dirPath := range dirPaths[:len(dirPaths)-1] {
|
||||
incrementPath = filepath.Join(incrementPath, dirPath)
|
||||
dirClient = client.NewDirectoryClient(incrementPath)
|
||||
if _, err := dirClient.Create(ctx, nil); err != nil {
|
||||
if !strings.Contains(err.Error(), "ResourceAlreadyExists") {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(source, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
fInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fSize := fInfo.Size()
|
||||
|
||||
fileName := filepath.Base(dest)
|
||||
fClient := dirClient.NewFileClient(fileName)
|
||||
if _, err := fClient.Create(ctx, fSize, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return fClient.UploadFile(ctx, file, nil)
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/common/compare"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/common/convert"
|
||||
)
|
||||
|
||||
// SubscriptionsService allows querying of subscriptions and tenants.
|
||||
type SubscriptionsService struct {
|
||||
credential azcore.TokenCredential
|
||||
armClientOptions *arm.ClientOptions
|
||||
}
|
||||
|
||||
func NewSubscriptionsService(
|
||||
credential azcore.TokenCredential,
|
||||
armClientOptions *arm.ClientOptions,
|
||||
) *SubscriptionsService {
|
||||
return &SubscriptionsService{
|
||||
credential: credential,
|
||||
armClientOptions: armClientOptions,
|
||||
}
|
||||
}
|
||||
|
||||
func (ss *SubscriptionsService) createSubscriptionsClient(
|
||||
ctx context.Context,
|
||||
tenantId string,
|
||||
) (*armsubscriptions.Client, error) {
|
||||
|
||||
client, err := armsubscriptions.NewClient(ss.credential, ss.armClientOptions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating subscriptions client: %w", err)
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (ss *SubscriptionsService) createTenantsClient(ctx context.Context) (*armsubscriptions.TenantsClient, error) {
|
||||
// Use default home tenant, since tenants itself can be listed across tenants
|
||||
client, err := armsubscriptions.NewTenantsClient(ss.credential, ss.armClientOptions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating tenants client: %w", err)
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (s *SubscriptionsService) ListSubscriptions(
|
||||
ctx context.Context,
|
||||
tenantId string,
|
||||
) ([]*armsubscriptions.Subscription, error) {
|
||||
client, err := s.createSubscriptionsClient(ctx, tenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subscriptions := []*armsubscriptions.Subscription{}
|
||||
pager := client.NewListPager(nil)
|
||||
|
||||
for pager.More() {
|
||||
page, err := pager.NextPage(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed getting next page of subscriptions: %w", err)
|
||||
}
|
||||
|
||||
subscriptions = append(subscriptions, page.SubscriptionListResult.Value...)
|
||||
}
|
||||
|
||||
sort.Slice(subscriptions, func(i, j int) bool {
|
||||
return *subscriptions[i].DisplayName < *subscriptions[j].DisplayName
|
||||
})
|
||||
|
||||
return subscriptions, nil
|
||||
}
|
||||
|
||||
func (s *SubscriptionsService) GetSubscription(
|
||||
ctx context.Context, subscriptionId string, tenantId string) (*armsubscriptions.Subscription, error) {
|
||||
client, err := s.createSubscriptionsClient(ctx, tenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subscription, err := client.Get(ctx, subscriptionId, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed getting subscription for '%s'", subscriptionId)
|
||||
}
|
||||
|
||||
return &subscription.Subscription, nil
|
||||
}
|
||||
|
||||
// ListSubscriptionLocations lists physical locations in Azure for the given subscription.
|
||||
func (s *SubscriptionsService) ListSubscriptionLocations(
|
||||
ctx context.Context, subscriptionId string, tenantId string) ([]Location, error) {
|
||||
client, err := s.createSubscriptionsClient(ctx, tenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
locations := []Location{}
|
||||
pager := client.NewListLocationsPager(subscriptionId, nil)
|
||||
|
||||
for pager.More() {
|
||||
page, err := pager.NextPage(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed getting next page of locations: %w", err)
|
||||
}
|
||||
|
||||
for _, location := range page.LocationListResult.Value {
|
||||
// Only include physical locations
|
||||
if *location.Metadata.RegionType == "Physical" &&
|
||||
!compare.PtrValueEquals(location.Metadata.PhysicalLocation, "") {
|
||||
displayName := convert.ToValueWithDefault(location.DisplayName, *location.Name)
|
||||
regionalDisplayName := convert.ToValueWithDefault(location.RegionalDisplayName, displayName)
|
||||
|
||||
locations = append(locations, Location{
|
||||
Name: *location.Name,
|
||||
DisplayName: displayName,
|
||||
RegionalDisplayName: regionalDisplayName,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(locations, func(i, j int) bool {
|
||||
return locations[i].RegionalDisplayName < locations[j].RegionalDisplayName
|
||||
})
|
||||
|
||||
return locations, nil
|
||||
}
|
||||
|
||||
func (s *SubscriptionsService) ListTenants(ctx context.Context) ([]armsubscriptions.TenantIDDescription, error) {
|
||||
client, err := s.createTenantsClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tenants := []armsubscriptions.TenantIDDescription{}
|
||||
pager := client.NewListPager(nil)
|
||||
|
||||
for pager.More() {
|
||||
page, err := pager.NextPage(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed getting next page of tenants: %w", err)
|
||||
}
|
||||
|
||||
for _, tenant := range page.TenantListResult.Value {
|
||||
if tenant != nil && tenant.TenantID != nil {
|
||||
tenants = append(tenants, *tenant)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(tenants, func(i, j int) bool {
|
||||
return convert.ToValueWithDefault(tenants[i].DisplayName, "") <
|
||||
convert.ToValueWithDefault(tenants[j].DisplayName, "")
|
||||
})
|
||||
|
||||
return tenants, nil
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package compare
|
||||
|
||||
import "strings"
|
||||
|
||||
// IsStringNilOrEmpty returns true if the string pointer is nil or the trimmed string is empty.
|
||||
func IsStringNilOrEmpty(value *string) bool {
|
||||
return value == nil || strings.TrimSpace(*value) == ""
|
||||
}
|
||||
|
||||
// PtrValueEquals returns true if the pointer is not nil and the value is equal to the expected value.
|
||||
func PtrValueEquals[T comparable](actual *T, expected T) bool {
|
||||
return actual != nil && *actual == expected
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package convert
|
||||
|
||||
// Converts a pointer to a value type
|
||||
// If the ptr is nil returns default value, otherwise the value of value of the pointer
|
||||
func ToValueWithDefault[T any](ptr *T, defaultValue T) T {
|
||||
if ptr == nil {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
if str, ok := any(ptr).(*string); ok && *str == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return *ptr
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/drone/envsubst"
|
||||
)
|
||||
|
||||
func NewExpandableString(template string) ExpandableString {
|
||||
return ExpandableString{
|
||||
template: template,
|
||||
}
|
||||
}
|
||||
|
||||
// ExpandableString is a string that has ${foo} style references inside which can be evaluated.
|
||||
type ExpandableString struct {
|
||||
template string
|
||||
}
|
||||
|
||||
// Empty returns true if the template is empty.
|
||||
func (e ExpandableString) Empty() bool {
|
||||
return e.template == ""
|
||||
}
|
||||
|
||||
// Envsubst evaluates the template, substituting values as [envsubst.Eval] would.
|
||||
func (e ExpandableString) Envsubst(mapping func(string) string) (string, error) {
|
||||
return envsubst.Eval(e.template, mapping)
|
||||
}
|
||||
|
||||
// MustEnvsubst evaluates the template, substituting values as [envsubst.Eval] would and panics if there
|
||||
// is an error (for example, the string is malformed).
|
||||
func (e ExpandableString) MustEnvsubst(mapping func(string) string) string {
|
||||
if v, err := envsubst.Eval(e.template, mapping); err != nil {
|
||||
panic(fmt.Sprintf("MustEnvsubst: %v", err))
|
||||
} else {
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func (e ExpandableString) MarshalYAML() (interface{}, error) {
|
||||
return e.template, nil
|
||||
}
|
||||
|
||||
func (e *ExpandableString) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var s string
|
||||
if err := unmarshal(&s); err != nil {
|
||||
return err
|
||||
}
|
||||
e.template = s
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package ioc
|
||||
|
||||
// binding represents the metadata used for an IoC registration consisting of a optional name and resolver.
|
||||
type binding struct {
|
||||
name string
|
||||
resolver any
|
||||
}
|
|
@ -0,0 +1,306 @@
|
|||
// This package wraps the golobby/container package to provide support for the following:
|
||||
// 1. Easier usage of lazy type resolvers and ability to register specific type instances
|
||||
// 2. Support for hierarchical/nested containers to resolve types from parent containers
|
||||
// 3. Helper methods for easier/streamlined usage of of the IoC container
|
||||
package ioc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
|
||||
"github.com/golobby/container/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
// The golobby project does not support types errors,
|
||||
// but all the error messages are prefixed with `container:`
|
||||
containerErrorRegex *regexp.Regexp = regexp.MustCompile("container:")
|
||||
|
||||
ErrResolveInstance error = errors.New("failed resolving instance from container")
|
||||
)
|
||||
|
||||
// NestedContainer is an IoC container that support nested containers
|
||||
// Used for more complex registration scenarios such as scop based registration/resolution.
|
||||
type NestedContainer struct {
|
||||
inner container.Container
|
||||
scopedBindings []*binding
|
||||
}
|
||||
|
||||
// Creates a new nested container from the specified parent container
|
||||
func NewNestedContainer(parent *NestedContainer) *NestedContainer {
|
||||
current := container.New()
|
||||
|
||||
if parent != nil {
|
||||
// Copy the bindings to the new container
|
||||
// The bindings hold the concrete instance of singleton registrations
|
||||
for key, value := range parent.inner {
|
||||
current[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
instance := &NestedContainer{
|
||||
inner: current,
|
||||
}
|
||||
|
||||
RegisterInstance[ServiceLocator](instance, instance)
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
// Creates a new container with only registrations from the given container.
|
||||
func NewRegistrationsOnly(from *NestedContainer) *NestedContainer {
|
||||
current := container.New()
|
||||
|
||||
if from != nil {
|
||||
// Reset all concrete instances by copying 'resolver' and 'isSingleton' fields
|
||||
// Reflection is necessary since *container.binding is unexported
|
||||
for key, value := range from.inner {
|
||||
var valueType = reflect.TypeOf(value)
|
||||
newValue := reflect.MakeMapWithSize(valueType, len(value))
|
||||
|
||||
for name, binding := range value {
|
||||
bindingVal := reflect.ValueOf(binding).Elem()
|
||||
|
||||
newBinding := reflect.New(reflect.TypeOf(binding).Elem())
|
||||
setUnexportedField(
|
||||
newBinding.Elem().FieldByName("resolver"),
|
||||
getUnexportedField(bindingVal.FieldByName("resolver")))
|
||||
setUnexportedField(
|
||||
newBinding.Elem().FieldByName("isSingleton"),
|
||||
getUnexportedField(bindingVal.FieldByName("isSingleton")))
|
||||
|
||||
n := name
|
||||
newValue.SetMapIndex(reflect.ValueOf(n), newBinding)
|
||||
}
|
||||
|
||||
reflect.ValueOf(current).SetMapIndex(reflect.ValueOf(key), newValue)
|
||||
}
|
||||
}
|
||||
|
||||
instance := &NestedContainer{
|
||||
inner: current,
|
||||
}
|
||||
|
||||
RegisterInstance[ServiceLocator](instance, instance)
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
func getUnexportedField(field reflect.Value) interface{} {
|
||||
return reflect.NewAt(field.Type(), field.Addr().UnsafePointer()).Elem().Interface()
|
||||
}
|
||||
|
||||
func setUnexportedField(field reflect.Value, value interface{}) {
|
||||
reflect.NewAt(field.Type(), field.Addr().UnsafePointer()).
|
||||
Elem().
|
||||
Set(reflect.ValueOf(value))
|
||||
}
|
||||
|
||||
// Fill takes a structure and resolves fields with the tag `container:"type" or `container:"name"`.
|
||||
func (c *NestedContainer) Fill(structure any) error {
|
||||
return c.inner.Fill(structure)
|
||||
}
|
||||
|
||||
// Registers a resolver with a singleton lifetime
|
||||
// Returns an error if the resolver is not valid
|
||||
func (c *NestedContainer) RegisterSingleton(resolveFn any) error {
|
||||
return c.inner.SingletonLazy(resolveFn)
|
||||
}
|
||||
|
||||
// Registers a resolver with a singleton lifetime
|
||||
// Panics if the resolver is not valid
|
||||
func (c *NestedContainer) MustRegisterSingleton(resolveFn any) {
|
||||
container.MustSingletonLazy(c.inner, resolveFn)
|
||||
}
|
||||
|
||||
// Registers a resolver with a singleton lifetime and instantiates the instance
|
||||
// Instance is stored in container cache is used for future resolutions
|
||||
// Returns an error if the resolver cannot instantiate the type
|
||||
func (c *NestedContainer) RegisterSingletonAndInvoke(resolveFn any) error {
|
||||
return c.inner.Singleton(resolveFn)
|
||||
}
|
||||
|
||||
// Registers a named resolver with a singleton lifetime
|
||||
// Returns an error if the resolver is not valid
|
||||
func (c *NestedContainer) RegisterNamedSingleton(name string, resolveFn any) error {
|
||||
return c.inner.NamedSingletonLazy(name, resolveFn)
|
||||
}
|
||||
|
||||
// Registers a named resolver with a singleton lifetime
|
||||
// Panics if the resolver is not valid
|
||||
func (c *NestedContainer) MustRegisterNamedSingleton(name string, resolveFn any) {
|
||||
container.MustNamedSingletonLazy(c.inner, name, resolveFn)
|
||||
}
|
||||
|
||||
// Registers a resolver with a transient lifetime (instance per resolution)
|
||||
// Returns an error if the resolver is not valid
|
||||
func (c *NestedContainer) RegisterTransient(resolveFn any) error {
|
||||
return c.inner.TransientLazy(resolveFn)
|
||||
}
|
||||
|
||||
// Registers a named resolver with a singleton lifetime and instantiates the instance
|
||||
// Panics if the resolver is not valid
|
||||
func (c *NestedContainer) MustRegisterTransient(resolveFn any) {
|
||||
container.MustTransientLazy(c.inner, resolveFn)
|
||||
}
|
||||
|
||||
// Registers a named resolver with a transient lifetime (instance per resolution)
|
||||
// Returns an error if the resolver is not valid
|
||||
func (c *NestedContainer) RegisterNamedTransient(name string, resolveFn any) error {
|
||||
return c.inner.NamedTransientLazy(name, resolveFn)
|
||||
}
|
||||
|
||||
// Registers a named resolver with a transient lifetime (instance per resolution)
|
||||
// Panics if the resolver is not valid
|
||||
func (c *NestedContainer) MustRegisterNamedTransient(name string, resolveFn any) {
|
||||
container.MustNamedTransientLazy(c.inner, name, resolveFn)
|
||||
}
|
||||
|
||||
// Registers a resolver with a scoped lifetime (instance per scope)
|
||||
// Ex: Each new cobra command will create a new scope
|
||||
// Scoped registrations are added as singletons in the current container then are reset in any new child containers
|
||||
// Returns an error if the resolver is not valid
|
||||
func (c *NestedContainer) RegisterScoped(resolveFn any) error {
|
||||
if err := c.inner.SingletonLazy(resolveFn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.scopedBindings = append(c.scopedBindings, &binding{
|
||||
resolver: resolveFn,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Registers a resolver with a scoped lifetime (instance per scope)
|
||||
// Ex: Each new cobra command will create a new scope
|
||||
// Scoped registrations are added as singletons in the current container then are reset in any new child containers
|
||||
// Panics if the resolver is not valid
|
||||
func (c *NestedContainer) MustRegisterScoped(resolveFn any) {
|
||||
if err := c.RegisterScoped(resolveFn); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Registers a named resolver with a scoped lifetime (instance per scope)
|
||||
// Ex: Each new cobra command will create a new scope
|
||||
// Scoped registrations are added as singletons in the current container then are reset in any new child containers
|
||||
func (c *NestedContainer) RegisterNamedScoped(name string, resolveFn any) error {
|
||||
if err := c.inner.NamedSingletonLazy(name, resolveFn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.scopedBindings = append(c.scopedBindings, &binding{
|
||||
name: name,
|
||||
resolver: resolveFn,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Registers a named resolver with a scoped lifetime (instance per scope)
|
||||
// Ex: Each new cobra command will create a new scope
|
||||
// Scoped registrations are added as singletons in the current container then are reset in any new child containers
|
||||
// Panics if the resolver is not valid
|
||||
func (c *NestedContainer) MustRegisterNamedScoped(name string, resolveFn any) {
|
||||
if err := c.RegisterNamedScoped(name, resolveFn); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Resolves an instance for the specified type
|
||||
// Returns an error if the resolution fails
|
||||
func (c *NestedContainer) Resolve(instance any) error {
|
||||
if err := c.inner.Resolve(instance); err != nil {
|
||||
return inspectResolveError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resolves a named instance for the specified type
|
||||
// Returns an error if the resolution fails
|
||||
func (c *NestedContainer) ResolveNamed(name string, instance any) error {
|
||||
if err := c.inner.NamedResolve(instance, name); err != nil {
|
||||
return inspectResolveError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Invokes the specified function and resolves any arguments specified
|
||||
// from the container resolver registrations
|
||||
func (c *NestedContainer) Invoke(resolver any) error {
|
||||
return c.inner.Call(resolver)
|
||||
}
|
||||
|
||||
// Registers a constructed instance of the specified type
|
||||
// Panics if the registration fails
|
||||
func RegisterInstance[F any](c *NestedContainer, instance F) {
|
||||
container.MustSingletonLazy(c.inner, func() F {
|
||||
return instance
|
||||
})
|
||||
}
|
||||
|
||||
// Registers a named constructed instance of the specified type
|
||||
// Panics if the registration fails
|
||||
func RegisterNamedInstance[F any](c *NestedContainer, name string, instance F) {
|
||||
container.MustNamedSingletonLazy(c.inner, name, func() F {
|
||||
return instance
|
||||
})
|
||||
}
|
||||
|
||||
// NewScope creates a new nested container with a relationship back to the parent container
|
||||
// Scoped registrations are converted to singleton registrations within the new nested container.
|
||||
func (c *NestedContainer) NewScope() (*NestedContainer, error) {
|
||||
childContainer := NewNestedContainer(c)
|
||||
|
||||
for _, binding := range c.scopedBindings {
|
||||
if binding.name == "" {
|
||||
if err := childContainer.RegisterSingleton(binding.resolver); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if err := childContainer.RegisterNamedSingleton(binding.name, binding.resolver); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
childContainer.scopedBindings = append(childContainer.scopedBindings, binding)
|
||||
}
|
||||
|
||||
return childContainer, nil
|
||||
}
|
||||
|
||||
// NewScopeRegistrationsOnly creates a new container with bindings deep copied from the container.
|
||||
// Scoped registrations are then activated as singletons within the new nested container.
|
||||
func (c *NestedContainer) NewScopeRegistrationsOnly() (*NestedContainer, error) {
|
||||
childContainer := NewRegistrationsOnly(c)
|
||||
|
||||
for _, binding := range c.scopedBindings {
|
||||
if binding.name == "" {
|
||||
if err := childContainer.RegisterSingleton(binding.resolver); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if err := childContainer.RegisterNamedSingleton(binding.name, binding.resolver); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
childContainer.scopedBindings = append(childContainer.scopedBindings, binding)
|
||||
}
|
||||
|
||||
return childContainer, nil
|
||||
}
|
||||
|
||||
// Inspects the specified error to determine whether the error is a
|
||||
// developer container registration error or an error that was
|
||||
// returned while instantiating a dependency.
|
||||
func inspectResolveError(err error) error {
|
||||
if containerErrorRegex.Match([]byte(err.Error())) {
|
||||
return fmt.Errorf("%w: %w", ErrResolveInstance, err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package ioc
|
||||
|
||||
type ServiceLocator interface {
|
||||
Resolve(instance any) error
|
||||
ResolveNamed(name string, instance any) error
|
||||
Invoke(resolver any) error
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package permissions
|
||||
|
||||
import "os"
|
||||
|
||||
const (
|
||||
PermissionDirectory os.FileMode = 0755
|
||||
PermissionExecutableFile os.FileMode = 0755
|
||||
PermissionFile os.FileMode = 0644
|
||||
|
||||
PermissionDirectoryOwnerOnly os.FileMode = 0700
|
||||
PermissionFileOwnerOnly os.FileMode = 0600
|
||||
|
||||
PermissionMaskDirectoryExecute os.FileMode = 0100
|
||||
)
|
|
@ -0,0 +1,66 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// WithOrder is like map, but also retains information about the order the keys of the object where in
|
||||
// when it was unmarshalled from JSON.
|
||||
type WithOrder[T any] struct {
|
||||
innerMap map[string]*T
|
||||
keys []string
|
||||
}
|
||||
|
||||
// Keys returns the keys of the map in the order they were unmarshalled from JSON.
|
||||
func (b *WithOrder[T]) OrderedKeys() []string {
|
||||
return b.keys
|
||||
}
|
||||
|
||||
// OrderedValues returns the values of the map in the order they were unmarshalled from JSON.
|
||||
func (b *WithOrder[T]) OrderedValues() []*T {
|
||||
values := make([]*T, len(b.keys))
|
||||
for i, key := range b.keys {
|
||||
values[i] = b.innerMap[key]
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
// Get returns the value associated with the given key, and a boolean indicating whether the key was present.
|
||||
func (b *WithOrder[T]) Get(key string) (*T, bool) {
|
||||
v, ok := b.innerMap[key]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func (b *WithOrder[T]) UnmarshalJSON(data []byte) error {
|
||||
if err := json.Unmarshal(data, &b.innerMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dec := json.NewDecoder(bytes.NewReader(data))
|
||||
|
||||
// read the start of the object
|
||||
_, err := dec.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
// read key or end
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tok == json.Delim('}') {
|
||||
return nil
|
||||
} else {
|
||||
b.keys = append(b.keys, tok.(string))
|
||||
}
|
||||
|
||||
// read binding value (and discard it, we already unmarshalled it into b.bindings)
|
||||
var b T
|
||||
if err := dec.Decode(&b); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,335 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
// Package config provides functionality related to storing application-wide configuration data.
|
||||
//
|
||||
// Configuration data stored should not be specific to a given repository/project.
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
//
|
||||
//nolint:lll
|
||||
var vaultPattern = regexp.MustCompile(
|
||||
`^vault://[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}/[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$`,
|
||||
)
|
||||
|
||||
// Azd configuration for the current user
|
||||
// Configuration data is stored in user's home directory @ ~/.azd/config.json
|
||||
type Config interface {
|
||||
Raw() map[string]any
|
||||
// similar to Raw() but it will resolve any vault references
|
||||
ResolvedRaw() map[string]any
|
||||
// Get retrieves the value stored at the specified path
|
||||
Get(path string) (any, bool)
|
||||
// GetString retrieves the value stored at the specified path as a string
|
||||
GetString(path string) (string, bool)
|
||||
// GetSection retrieves the value stored at the specified path and unmarshals it into the provided section
|
||||
GetSection(path string, section any) (bool, error)
|
||||
// GetMap retrieves the map stored at the specified path
|
||||
GetMap(path string) (map[string]any, bool)
|
||||
// GetSlice retrieves the slice stored at the specified path
|
||||
GetSlice(path string) ([]any, bool)
|
||||
// Set stores the value at the specified path
|
||||
Set(path string, value any) error
|
||||
// SetSecret stores the secrets at the specified path within a local user vault
|
||||
SetSecret(path string, value string) error
|
||||
// Unset removes the value stored at the specified path
|
||||
Unset(path string) error
|
||||
// IsEmpty returns a value indicating whether the configuration is empty
|
||||
IsEmpty() bool
|
||||
}
|
||||
|
||||
// NewEmptyConfig creates a empty configuration object.
|
||||
func NewEmptyConfig() Config {
|
||||
return NewConfig(nil)
|
||||
}
|
||||
|
||||
// NewConfig creates a configuration object, populated with an initial set of keys and values. If [data] is nil or an
|
||||
// empty map, and empty configuration object is returned, but [NewEmptyConfig] might better express your intention.
|
||||
func NewConfig(data map[string]any) Config {
|
||||
if data == nil {
|
||||
data = map[string]any{}
|
||||
}
|
||||
|
||||
return &config{
|
||||
data: data,
|
||||
}
|
||||
}
|
||||
|
||||
// Top level AZD configuration
|
||||
type config struct {
|
||||
vaultId string
|
||||
vault Config
|
||||
data map[string]any
|
||||
}
|
||||
|
||||
// Returns a value indicating whether the configuration is empty
|
||||
func (c *config) IsEmpty() bool {
|
||||
return len(c.data) == 0
|
||||
}
|
||||
|
||||
// Gets the raw values stored in the configuration as a Go map
|
||||
func (c *config) Raw() map[string]any {
|
||||
return c.data
|
||||
}
|
||||
|
||||
const vaultKeyName = "vault"
|
||||
|
||||
// Gets the raw values stored in the configuration and resolve any vault references
|
||||
func (c *config) ResolvedRaw() map[string]any {
|
||||
resolvedRaw := &config{
|
||||
data: map[string]any{},
|
||||
}
|
||||
paths := paths(c.data)
|
||||
for _, path := range paths {
|
||||
if path == vaultKeyName {
|
||||
// a resolved raw should not include a reference a vault, as all secrets should be resolved
|
||||
// when a config file contains a vault reference and the vault is not found, azd returns os.ErrNotExist
|
||||
// and to the eyes of components using a Config, that means the config does not exists.
|
||||
continue
|
||||
}
|
||||
// get will always return true (no need to check) because the path was gotten from the raw config
|
||||
value, _ := c.Get(path)
|
||||
if err := resolvedRaw.Set(path, value); err != nil {
|
||||
panic(fmt.Errorf("failed setting resolved raw value: %w", err))
|
||||
}
|
||||
}
|
||||
return resolvedRaw.data
|
||||
}
|
||||
|
||||
// paths recursively traverses a map and returns a list of all the paths to the leaf nodes.
|
||||
// The start parameter is the initial map to start traversing from.
|
||||
// It returns a slice of strings representing the paths to the leaf nodes.
|
||||
func paths(start map[string]any) []string {
|
||||
var all []string
|
||||
for path, value := range start {
|
||||
if node, isNode := value.(map[string]any); isNode {
|
||||
for _, child := range paths(node) {
|
||||
all = append(all, fmt.Sprintf("%s.%s", path, child))
|
||||
}
|
||||
} else {
|
||||
all = append(all, path)
|
||||
}
|
||||
}
|
||||
return all
|
||||
}
|
||||
|
||||
// SetSecret stores the secrets at the specified path within a local user vault
|
||||
func (c *config) SetSecret(path string, value string) error {
|
||||
if c.vaultId == "" {
|
||||
c.vault = NewConfig(nil)
|
||||
c.vaultId = uuid.New().String()
|
||||
if err := c.Set(vaultKeyName, c.vaultId); err != nil {
|
||||
return fmt.Errorf("failed setting vault id: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
pathId := uuid.New().String()
|
||||
vaultRef := fmt.Sprintf("vault://%s/%s", c.vaultId, pathId)
|
||||
if err := c.vault.Set(pathId, base64.StdEncoding.EncodeToString([]byte(value))); err != nil {
|
||||
return fmt.Errorf("failed setting secret value: %w", err)
|
||||
}
|
||||
|
||||
return c.Set(path, vaultRef)
|
||||
}
|
||||
|
||||
// Sets a value at the specified location
|
||||
func (c *config) Set(path string, value any) error {
|
||||
depth := 1
|
||||
currentNode := c.data
|
||||
parts := strings.Split(path, ".")
|
||||
for _, part := range parts {
|
||||
if depth == len(parts) {
|
||||
currentNode[part] = value
|
||||
return nil
|
||||
}
|
||||
var node map[string]any
|
||||
value, ok := currentNode[part]
|
||||
if !ok || value == nil {
|
||||
node = map[string]any{}
|
||||
}
|
||||
|
||||
if value != nil {
|
||||
node, ok = value.(map[string]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("failed converting node at path '%s' to map", part)
|
||||
}
|
||||
}
|
||||
|
||||
currentNode[part] = node
|
||||
currentNode = node
|
||||
depth++
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Removes any values stored at the specified path
|
||||
// When the path location is an object will remove the whole node
|
||||
// When the path does not exist, will return a `nil` value
|
||||
func (c *config) Unset(path string) error {
|
||||
depth := 1
|
||||
currentNode := c.data
|
||||
parts := strings.Split(path, ".")
|
||||
for _, part := range parts {
|
||||
if depth == len(parts) {
|
||||
delete(currentNode, part)
|
||||
return nil
|
||||
}
|
||||
var node map[string]any
|
||||
value, ok := currentNode[part]
|
||||
|
||||
// Path already doesn't exist, NOOP
|
||||
if !ok || value == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
node, ok = value.(map[string]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("failed converting node at path '%s' to map", part)
|
||||
}
|
||||
|
||||
currentNode[part] = node
|
||||
currentNode = node
|
||||
depth++
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Gets the value stored at the specified location
|
||||
// Returns the value if exists, otherwise returns nil & a value indicating if the value existing
|
||||
func (c *config) Get(path string) (any, bool) {
|
||||
depth := 1
|
||||
currentNode := c.data
|
||||
parts := strings.Split(path, ".")
|
||||
for _, part := range parts {
|
||||
// When the depth is equal to the number of parts, we have reached the desired node path
|
||||
// At this point we can perform any final processing on the node and return the result
|
||||
if depth == len(parts) {
|
||||
value, ok := currentNode[part]
|
||||
if !ok {
|
||||
return value, ok
|
||||
}
|
||||
|
||||
return c.interpolateNodeValue(value)
|
||||
}
|
||||
value, ok := currentNode[part]
|
||||
if !ok {
|
||||
return value, ok
|
||||
}
|
||||
|
||||
node, ok := value.(map[string]any)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
currentNode = node
|
||||
depth++
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// GetMap retrieves the map stored at the specified path
|
||||
func (c *config) GetMap(path string) (map[string]any, bool) {
|
||||
value, ok := c.Get(path)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
node, ok := value.(map[string]any)
|
||||
return node, ok
|
||||
}
|
||||
|
||||
// GetSlice retrieves the slice stored at the specified path
|
||||
func (c *config) GetSlice(path string) ([]any, bool) {
|
||||
value, ok := c.Get(path)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
node, ok := value.([]any)
|
||||
return node, ok
|
||||
}
|
||||
|
||||
// Gets the value stored at the specified location as a string
|
||||
func (c *config) GetString(path string) (string, bool) {
|
||||
value, ok := c.Get(path)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
|
||||
str, ok := value.(string)
|
||||
return str, ok
|
||||
}
|
||||
|
||||
func (c *config) GetSection(path string, section any) (bool, error) {
|
||||
sectionConfig, ok := c.Get(path)
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(sectionConfig)
|
||||
if err != nil {
|
||||
return true, fmt.Errorf("marshalling section config: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(jsonBytes, section); err != nil {
|
||||
return true, fmt.Errorf("unmarshalling section config: %w", err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// getSecret retrieves the secret stored at the specified path from a local user vault
|
||||
func (c *config) getSecret(vaultRef string) (string, bool) {
|
||||
encodedValue, ok := c.vault.GetString(filepath.Base(vaultRef))
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
|
||||
bytes, err := base64.StdEncoding.DecodeString(encodedValue)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return string(bytes), true
|
||||
}
|
||||
|
||||
// interpolateNodeValue processes the node, iterates on any nested nodes and interpolates any vault references
|
||||
func (c *config) interpolateNodeValue(value any) (any, bool) {
|
||||
// Check if the value is a vault reference
|
||||
// If it is, retrieve the secret from the vault
|
||||
if vaultRef, isString := value.(string); isString && vaultPattern.MatchString(vaultRef) {
|
||||
return c.getSecret(vaultRef)
|
||||
}
|
||||
|
||||
// If the value is a map, recursively iterate over the map and interpolate the values
|
||||
if node, isMap := value.(map[string]any); isMap {
|
||||
// We want to ensure we return a cloned map so that we don't modify the original data
|
||||
// stored within the config map data structure
|
||||
cloneMap := map[string]any{}
|
||||
|
||||
for key, val := range node {
|
||||
if nodeValue, ok := c.interpolateNodeValue(val); ok {
|
||||
cloneMap[key] = nodeValue
|
||||
}
|
||||
}
|
||||
|
||||
return cloneMap, true
|
||||
}
|
||||
|
||||
// Finally, if the value is not handled above we can return the value as is
|
||||
return value, true
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/common/permissions"
|
||||
)
|
||||
|
||||
// FileConfigManager provides the ability to load, parse and save azd configuration files
|
||||
type FileConfigManager interface {
|
||||
// Saves the azd configuration to the specified file path
|
||||
// Path is automatically created if it does not exist
|
||||
Save(config Config, filePath string) error
|
||||
|
||||
// Loads azd configuration from the specified file path
|
||||
Load(filePath string) (Config, error)
|
||||
}
|
||||
|
||||
// NewFileConfigManager creates a new FileConfigManager instance
|
||||
func NewFileConfigManager(configManager Manager) FileConfigManager {
|
||||
return &fileConfigManager{
|
||||
manager: configManager,
|
||||
}
|
||||
}
|
||||
|
||||
type fileConfigManager struct {
|
||||
manager Manager
|
||||
}
|
||||
|
||||
func (m *fileConfigManager) Load(filePath string) (Config, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed opening azd configuration file: %w", err)
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
azdConfig, err := m.manager.Load(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the configuration contains a vault, then also load the vault configuration
|
||||
vaultId, ok := azdConfig.GetString(vaultKeyName)
|
||||
if ok {
|
||||
configPath, err := GetUserConfigDir()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed getting user config directory: %w", err)
|
||||
}
|
||||
|
||||
vaultPath := filepath.Join(configPath, "vaults", fmt.Sprintf("%s.json", vaultId))
|
||||
vaultConfig, err := m.Load(vaultPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed loading vault configuration from '%s': %w", vaultPath, err)
|
||||
}
|
||||
|
||||
baseConfig, ok := azdConfig.(*config)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed casting azd configuration to config")
|
||||
}
|
||||
|
||||
baseConfig.vaultId = vaultId
|
||||
baseConfig.vault = vaultConfig
|
||||
}
|
||||
|
||||
return azdConfig, nil
|
||||
}
|
||||
|
||||
func (m *fileConfigManager) Save(c Config, filePath string) error {
|
||||
folderPath := filepath.Dir(filePath)
|
||||
if err := os.MkdirAll(folderPath, permissions.PermissionDirectory); err != nil {
|
||||
return fmt.Errorf("failed creating config directory: %w", err)
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, permissions.PermissionFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed creating config directory: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
err = m.manager.Save(c, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
baseConfig, ok := c.(*config)
|
||||
if !ok {
|
||||
return fmt.Errorf("failed casting azd configuration to config")
|
||||
}
|
||||
|
||||
// If the configuration contains a vault, then also save the vault configuration
|
||||
// Vault configuration always gets saved in a separate file in the users HOME directory.
|
||||
if baseConfig.vaultId != "" {
|
||||
configPath, err := GetUserConfigDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed getting user config directory: %w", err)
|
||||
}
|
||||
|
||||
vaultPath := filepath.Join(configPath, "vaults", fmt.Sprintf("%s.json", baseConfig.vaultId))
|
||||
if err = os.MkdirAll(filepath.Dir(vaultPath), permissions.PermissionDirectory); err != nil {
|
||||
return fmt.Errorf("failed creating vaults directory: %w", err)
|
||||
}
|
||||
|
||||
return m.Save(baseConfig.vault, vaultPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/common/permissions"
|
||||
)
|
||||
|
||||
// Config Manager provides the ability to load, parse and save azd configuration files
|
||||
type manager struct {
|
||||
}
|
||||
|
||||
type Manager interface {
|
||||
Save(config Config, writer io.Writer) error
|
||||
Load(io.Reader) (Config, error)
|
||||
}
|
||||
|
||||
// Creates a new Configuration Manager
|
||||
func NewManager() Manager {
|
||||
return &manager{}
|
||||
}
|
||||
|
||||
// Saves the azd configuration to the specified file path
|
||||
func (c *manager) Save(config Config, writer io.Writer) error {
|
||||
configJson, err := json.MarshalIndent(config.Raw(), "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed marshalling config JSON: %w", err)
|
||||
}
|
||||
|
||||
_, err = writer.Write(configJson)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed writing configuration data: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Loads azd configuration from the specified file path
|
||||
func (c *manager) Load(reader io.Reader) (Config, error) {
|
||||
jsonBytes, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed reading azd configuration file")
|
||||
}
|
||||
|
||||
return Parse(jsonBytes)
|
||||
}
|
||||
|
||||
// Parses azd configuration JSON and returns a Config instance
|
||||
func Parse(configJson []byte) (Config, error) {
|
||||
var data map[string]any
|
||||
err := json.Unmarshal(configJson, &data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed unmarshalling configuration JSON: %w", err)
|
||||
}
|
||||
|
||||
return NewConfig(data), nil
|
||||
}
|
||||
|
||||
// GetUserConfigDir returns the config directory for storing user wide configuration data.
|
||||
//
|
||||
// The config directory is guaranteed to exist, otherwise an error is returned.
|
||||
func GetUserConfigDir() (string, error) {
|
||||
configDirPath := os.Getenv("AZD_CONFIG_DIR")
|
||||
if configDirPath == "" {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not determine current home directory: %w", err)
|
||||
}
|
||||
|
||||
configDirPath = filepath.Join(homeDir, ".azd")
|
||||
}
|
||||
|
||||
err := os.MkdirAll(configDirPath, permissions.PermissionDirectoryOwnerOnly)
|
||||
if err != nil {
|
||||
return configDirPath, err
|
||||
}
|
||||
|
||||
// Ensure that the "x" permission is set on the folder for the current
|
||||
// user. In cases where the config directory is ~/.azd, OS upgrades and
|
||||
// other processes can remove the "x" permission
|
||||
if runtime.GOOS == "linux" || runtime.GOOS == "darwin" {
|
||||
info, err := os.Stat(configDirPath)
|
||||
if err != nil {
|
||||
return configDirPath, err
|
||||
}
|
||||
|
||||
filePermissions := info.Mode().Perm()
|
||||
if filePermissions&permissions.PermissionMaskDirectoryExecute == 0 {
|
||||
// Ensure user execute permissions
|
||||
err := os.Chmod(configDirPath, filePermissions|permissions.PermissionMaskDirectoryExecute)
|
||||
return configDirPath, err
|
||||
}
|
||||
}
|
||||
|
||||
return configDirPath, err
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type UserConfig Config
|
||||
|
||||
type UserConfigManager interface {
|
||||
Save(UserConfig) error
|
||||
Load() (UserConfig, error)
|
||||
}
|
||||
|
||||
type userConfigManager struct {
|
||||
manager FileConfigManager
|
||||
}
|
||||
|
||||
func NewUserConfigManager(configManager FileConfigManager) UserConfigManager {
|
||||
return &userConfigManager{
|
||||
manager: configManager,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *userConfigManager) Load() (UserConfig, error) {
|
||||
var azdConfig Config
|
||||
|
||||
configFilePath, err := GetUserConfigFilePath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
azdConfig, err = m.manager.Load(configFilePath)
|
||||
if err != nil {
|
||||
// Ignore missing file errors
|
||||
// File will automatically be created on first `set` operation
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
log.Printf("creating empty config since '%s' did not exist.", configFilePath)
|
||||
return NewConfig(nil), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("failed loading azd user config from '%s'. %w", configFilePath, err)
|
||||
}
|
||||
|
||||
return azdConfig, nil
|
||||
}
|
||||
|
||||
func (m *userConfigManager) Save(c UserConfig) error {
|
||||
userConfigFilePath, err := GetUserConfigFilePath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed getting user config file path. %w", err)
|
||||
}
|
||||
|
||||
err = m.manager.Save(c, userConfigFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed saving configuration. %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Gets the local file system path to the Azd configuration file
|
||||
func GetUserConfigFilePath() (string, error) {
|
||||
configPath, err := GetUserConfigDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed getting user config file path '%s'. %w", configPath, err)
|
||||
}
|
||||
|
||||
return filepath.Join(configPath, "config.json"), nil
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
package azdcore
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/common/permissions"
|
||||
)
|
||||
|
||||
const ProjectFileName = "azure.yaml"
|
||||
const EnvironmentDirectoryName = ".azure"
|
||||
const DotEnvFileName = ".env"
|
||||
const ConfigFileName = "config.json"
|
||||
const ConfigFileVersion = 1
|
||||
|
||||
type Context struct {
|
||||
projectDirectory string
|
||||
}
|
||||
|
||||
func (c *Context) ProjectDirectory() string {
|
||||
return c.projectDirectory
|
||||
}
|
||||
|
||||
func (c *Context) SetProjectDirectory(dir string) {
|
||||
c.projectDirectory = dir
|
||||
}
|
||||
|
||||
func (c *Context) ProjectPath() string {
|
||||
return filepath.Join(c.ProjectDirectory(), ProjectFileName)
|
||||
}
|
||||
|
||||
func (c *Context) EnvironmentDirectory() string {
|
||||
return filepath.Join(c.ProjectDirectory(), EnvironmentDirectoryName)
|
||||
}
|
||||
|
||||
func (c *Context) GetDefaultProjectName() string {
|
||||
return filepath.Base(c.ProjectDirectory())
|
||||
}
|
||||
|
||||
func (c *Context) EnvironmentRoot(name string) string {
|
||||
return filepath.Join(c.EnvironmentDirectory(), name)
|
||||
}
|
||||
|
||||
func (c *Context) GetEnvironmentWorkDirectory(name string) string {
|
||||
return filepath.Join(c.EnvironmentRoot(name), "wd")
|
||||
}
|
||||
|
||||
// GetDefaultEnvironmentName returns the name of the default environment. Returns
|
||||
// an empty string if a default environment has not been set.
|
||||
func (c *Context) GetDefaultEnvironmentName() (string, error) {
|
||||
path := filepath.Join(c.EnvironmentDirectory(), ConfigFileName)
|
||||
file, err := os.ReadFile(path)
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
return "", nil
|
||||
case err != nil:
|
||||
return "", fmt.Errorf("reading config file: %w", err)
|
||||
}
|
||||
|
||||
var config configFile
|
||||
if err := json.Unmarshal(file, &config); err != nil {
|
||||
return "", fmt.Errorf("deserializing config file: %w", err)
|
||||
}
|
||||
|
||||
return config.DefaultEnvironment, nil
|
||||
}
|
||||
|
||||
// ProjectState represents the state of the project.
|
||||
type ProjectState struct {
|
||||
DefaultEnvironment string
|
||||
}
|
||||
|
||||
// SetProjectState persists the state of the project to the file system, like the default environment.
|
||||
func (c *Context) SetProjectState(state ProjectState) error {
|
||||
path := filepath.Join(c.EnvironmentDirectory(), ConfigFileName)
|
||||
config := configFile{
|
||||
Version: ConfigFileVersion,
|
||||
DefaultEnvironment: state.DefaultEnvironment,
|
||||
}
|
||||
|
||||
if err := writeConfig(path, config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// make sure to ignore the environment directory
|
||||
path = filepath.Join(c.EnvironmentDirectory(), ".gitignore")
|
||||
return os.WriteFile(path, []byte("# .azure is not intended to be committed\n*"), permissions.PermissionFile)
|
||||
}
|
||||
|
||||
// Creates context with project directory set to the desired directory.
|
||||
func NewContextWithDirectory(projectDirectory string) *Context {
|
||||
return &Context{
|
||||
projectDirectory: projectDirectory,
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
ErrNoProject = errors.New("no project exists; to create a new project, run `azd init`")
|
||||
)
|
||||
|
||||
// Creates context with project directory set to the nearest project file found by calling NewAzdContextFromWd
|
||||
// on the current working directory.
|
||||
func NewContext() (*Context, error) {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get the current directory: %w", err)
|
||||
}
|
||||
|
||||
return NewContextFromWd(wd)
|
||||
}
|
||||
|
||||
// Creates context with project directory set to the nearest project file found.
|
||||
//
|
||||
// The project file is first searched for in the working directory, if not found, the parent directory is searched
|
||||
// recursively up to root. If no project file is found, errNoProject is returned.
|
||||
func NewContextFromWd(wd string) (*Context, error) {
|
||||
// Walk up from the wd to the root, looking for a project file. If we find one, that's
|
||||
// the root project directory.
|
||||
searchDir, err := filepath.Abs(wd)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolving path: %w", err)
|
||||
}
|
||||
|
||||
for {
|
||||
projectFilePath := filepath.Join(searchDir, ProjectFileName)
|
||||
stat, err := os.Stat(projectFilePath)
|
||||
if os.IsNotExist(err) || (err == nil && stat.IsDir()) {
|
||||
parent := filepath.Dir(searchDir)
|
||||
if parent == searchDir {
|
||||
return nil, ErrNoProject
|
||||
}
|
||||
searchDir = parent
|
||||
} else if err == nil {
|
||||
// We found our azure.yaml file, and searchDir is the directory
|
||||
// that contains it.
|
||||
break
|
||||
} else {
|
||||
return nil, fmt.Errorf("searching for project file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &Context{
|
||||
projectDirectory: searchDir,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type configFile struct {
|
||||
Version int `json:"version"`
|
||||
DefaultEnvironment string `json:"defaultEnvironment,omitempty"`
|
||||
}
|
||||
|
||||
func writeConfig(path string, config configFile) error {
|
||||
bytes, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("serializing config file: %w", err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(path), permissions.PermissionDirectory); err != nil {
|
||||
return fmt.Errorf("creating environment root: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, bytes, permissions.PermissionFile); err != nil {
|
||||
return fmt.Errorf("writing config file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package contracts
|
||||
|
||||
type AksOptions struct {
|
||||
// The namespace used for deploying k8s resources. Defaults to the project name
|
||||
Namespace string `yaml:"namespace"`
|
||||
// The relative folder path from the service that contains the k8s deployment manifests. Defaults to 'manifests'
|
||||
DeploymentPath string `yaml:"deploymentPath"`
|
||||
// The services ingress configuration options
|
||||
Ingress AksIngressOptions `yaml:"ingress"`
|
||||
// The services deployment configuration options
|
||||
Deployment AksDeploymentOptions `yaml:"deployment"`
|
||||
// The services service configuration options
|
||||
Service AksServiceOptions `yaml:"service"`
|
||||
// The helm configuration options
|
||||
Helm *HelmConfig `yaml:"helm"`
|
||||
// The kustomize configuration options
|
||||
Kustomize *KustomizeConfig `yaml:"kustomize"`
|
||||
}
|
||||
|
||||
// The AKS ingress options
|
||||
type AksIngressOptions struct {
|
||||
Name string `yaml:"name"`
|
||||
RelativePath string `yaml:"relativePath"`
|
||||
}
|
||||
|
||||
// The AKS deployment options
|
||||
type AksDeploymentOptions struct {
|
||||
Name string `yaml:"name"`
|
||||
}
|
||||
|
||||
// The AKS service configuration options
|
||||
type AksServiceOptions struct {
|
||||
Name string `yaml:"name"`
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package contracts
|
||||
|
||||
import "github.com/azure/azure-dev/cli/sdk/azdcore/common"
|
||||
|
||||
type DockerProjectOptions struct {
|
||||
Path string `yaml:"path,omitempty" json:"path,omitempty"`
|
||||
Context string `yaml:"context,omitempty" json:"context,omitempty"`
|
||||
Platform string `yaml:"platform,omitempty" json:"platform,omitempty"`
|
||||
Target string `yaml:"target,omitempty" json:"target,omitempty"`
|
||||
Registry common.ExpandableString `yaml:"registry,omitempty" json:"registry,omitempty"`
|
||||
Image common.ExpandableString `yaml:"image,omitempty" json:"image,omitempty"`
|
||||
Tag common.ExpandableString `yaml:"tag,omitempty" json:"tag,omitempty"`
|
||||
RemoteBuild bool `yaml:"remoteBuild,omitempty" json:"remoteBuild,omitempty"`
|
||||
BuildArgs []common.ExpandableString `yaml:"buildArgs,omitempty" json:"buildArgs,omitempty"`
|
||||
// not supported from azure.yaml directly yet. Adding it for Aspire to use it, initially.
|
||||
// Aspire would pass the secret keys, which are env vars that azd will set just to run docker build.
|
||||
BuildSecrets []string `yaml:"-" json:"-"`
|
||||
BuildEnv []string `yaml:"-" json:"-"`
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package contracts
|
||||
|
||||
type HelmConfig struct {
|
||||
Repositories []*HelmRepository `yaml:"repositories"`
|
||||
Releases []*HelmRelease `yaml:"releases"`
|
||||
}
|
||||
|
||||
type HelmRepository struct {
|
||||
Name string `yaml:"name"`
|
||||
Url string `yaml:"url"`
|
||||
}
|
||||
|
||||
type HelmRelease struct {
|
||||
Name string `yaml:"name"`
|
||||
Chart string `yaml:"chart"`
|
||||
Version string `yaml:"version"`
|
||||
Namespace string `yaml:"namespace"`
|
||||
Values string `yaml:"values"`
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
package contracts
|
||||
|
||||
import "fmt"
|
||||
|
||||
// The type of hooks. Supported values are 'pre' and 'post'
|
||||
type HookType string
|
||||
type HookPlatformType string
|
||||
type ShellType string
|
||||
type ScriptLocation string
|
||||
|
||||
const (
|
||||
ShellTypeBash ShellType = "sh"
|
||||
ShellTypePowershell ShellType = "pwsh"
|
||||
ScriptTypeUnknown ShellType = ""
|
||||
ScriptLocationInline ScriptLocation = "inline"
|
||||
ScriptLocationPath ScriptLocation = "path"
|
||||
ScriptLocationUnknown ScriptLocation = ""
|
||||
// Executes pre hooks
|
||||
HookTypePre HookType = "pre"
|
||||
// Execute post hooks
|
||||
HookTypePost HookType = "post"
|
||||
HookTypeNone HookType = ""
|
||||
HookPlatformWindows HookPlatformType = "windows"
|
||||
HookPlatformPosix HookPlatformType = "posix"
|
||||
)
|
||||
|
||||
type HookConfig struct {
|
||||
// The location of the script hook (file path or inline)
|
||||
location ScriptLocation
|
||||
// When location is `path` a file path must be specified relative to the project or service
|
||||
path string
|
||||
// Stores a value whether or not this hook config has been previously validated
|
||||
validated bool
|
||||
// Stores the working directory set for this hook config
|
||||
cwd string
|
||||
// When location is `inline` a script must be defined inline
|
||||
script string
|
||||
|
||||
// Internal name of the hook running for a given command
|
||||
Name string `yaml:",omitempty"`
|
||||
// The type of script hook (bash or powershell)
|
||||
Shell ShellType `yaml:"shell,omitempty"`
|
||||
// The inline script to execute or path to existing file
|
||||
Run string `yaml:"run,omitempty"`
|
||||
// When set to true will not halt command execution even when a script error occurs.
|
||||
ContinueOnError bool `yaml:"continueOnError,omitempty"`
|
||||
// When set to true will bind the stdin, stdout & stderr to the running console
|
||||
Interactive bool `yaml:"interactive,omitempty"`
|
||||
// When running on windows use this override config
|
||||
Windows *HookConfig `yaml:"windows,omitempty"`
|
||||
// When running on linux/macos use this override config
|
||||
Posix *HookConfig `yaml:"posix,omitempty"`
|
||||
}
|
||||
|
||||
// HooksConfig is an alias for map of hook names to slice of hook configurations
|
||||
// This custom alias type is used to help support YAML unmarshalling of legacy single hook configurations
|
||||
// and new multiple hook configurations
|
||||
type HooksConfig map[string][]*HookConfig
|
||||
|
||||
// UnmarshalYAML converts the hooks configuration from YAML supporting both legacy single hook configurations
|
||||
// and new multiple hook configurations
|
||||
func (ch *HooksConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var legacyConfig map[string]*HookConfig
|
||||
|
||||
// Attempt to unmarshal the legacy single hook configuration
|
||||
if err := unmarshal(&legacyConfig); err == nil {
|
||||
newConfig := HooksConfig{}
|
||||
|
||||
for key, value := range legacyConfig {
|
||||
newConfig[key] = []*HookConfig{value}
|
||||
}
|
||||
|
||||
*ch = newConfig
|
||||
} else { // Unmarshal the new multiple hook configuration
|
||||
var newConfig map[string][]*HookConfig
|
||||
if err := unmarshal(&newConfig); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal hooks configuration: %w", err)
|
||||
}
|
||||
|
||||
*ch = newConfig
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalYAML marshals the hooks configuration to YAML supporting both legacy single hook configurations
|
||||
func (ch HooksConfig) MarshalYAML() (interface{}, error) {
|
||||
if len(ch) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
result := map[string]any{}
|
||||
for key, hooks := range ch {
|
||||
if len(hooks) == 1 {
|
||||
result[key] = hooks[0]
|
||||
} else {
|
||||
result[key] = hooks
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package contracts
|
||||
|
||||
import "github.com/azure/azure-dev/cli/sdk/azdcore/common"
|
||||
|
||||
type KustomizeConfig struct {
|
||||
Directory common.ExpandableString `yaml:"dir"`
|
||||
Edits []common.ExpandableString `yaml:"edits"`
|
||||
Env map[string]common.ExpandableString `yaml:"env"`
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package contracts
|
||||
|
||||
// options supported in azure.yaml
|
||||
type PipelineOptions struct {
|
||||
Provider string `yaml:"provider"`
|
||||
Variables []string `yaml:"variables"`
|
||||
Secrets []string `yaml:"secrets"`
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package contracts
|
||||
|
||||
type PlatformKind string
|
||||
|
||||
type PlatformConfig struct {
|
||||
Type PlatformKind `yaml:"type"`
|
||||
Config map[string]any `yaml:"config"`
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package contracts
|
||||
|
||||
import "fmt"
|
||||
|
||||
type ProvisioningProviderKind string
|
||||
|
||||
const (
|
||||
NotSpecified ProvisioningProviderKind = ""
|
||||
Bicep ProvisioningProviderKind = "bicep"
|
||||
Arm ProvisioningProviderKind = "arm"
|
||||
Terraform ProvisioningProviderKind = "terraform"
|
||||
Pulumi ProvisioningProviderKind = "pulumi"
|
||||
Test ProvisioningProviderKind = "test"
|
||||
)
|
||||
|
||||
type ProvisioningOptions struct {
|
||||
Provider ProvisioningProviderKind `yaml:"provider,omitempty"`
|
||||
Path string `yaml:"path,omitempty"`
|
||||
Module string `yaml:"module,omitempty"`
|
||||
DeploymentStacks map[string]any `yaml:"deploymentStacks,omitempty"`
|
||||
// Not expected to be defined at azure.yaml
|
||||
IgnoreDeploymentState bool `yaml:"-"`
|
||||
}
|
||||
|
||||
// Parses the specified IaC Provider to ensure whether it is valid or not
|
||||
// Defaults to `Bicep` if no provider is specified
|
||||
func ParseProvisioningProvider(kind ProvisioningProviderKind) (ProvisioningProviderKind, error) {
|
||||
switch kind {
|
||||
// For the time being we need to include `Test` here for the unit tests to work as expected
|
||||
// App builds will pass this test but fail resolving the provider since `Test` won't be registered in the container
|
||||
case NotSpecified, Bicep, Terraform, Test:
|
||||
return kind, nil
|
||||
}
|
||||
|
||||
return ProvisioningProviderKind(""), fmt.Errorf("unsupported IaC provider '%s'", kind)
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package contracts
|
||||
|
||||
// The Azure Spring Apps configuration options
|
||||
type SpringOptions struct {
|
||||
// The deployment name of ASA app
|
||||
DeploymentName string `yaml:"deploymentName"`
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package contracts
|
||||
|
||||
type StateConfig struct {
|
||||
Remote *RemoteConfig `json:"remote" yaml:"remote"`
|
||||
}
|
||||
|
||||
// RemoteConfig is the state configuration for a remote backend
|
||||
type RemoteConfig struct {
|
||||
Backend string `json:"backend" yaml:"backend"`
|
||||
Config map[string]any `json:"config" yaml:"config"`
|
||||
}
|
|
@ -0,0 +1,168 @@
|
|||
package contracts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Stores a map of workflows configured for an azd project
|
||||
type WorkflowMap map[string]*Workflow
|
||||
|
||||
// UnmarshalYAML will unmarshal the WorkflowMap from YAML.
|
||||
// The unmarshalling will marshall the YAML like a standard Go map
|
||||
// but will also persist the key as the workflow name within the Workflow struct.
|
||||
func (wm *WorkflowMap) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var m map[string]*Workflow
|
||||
if err := unmarshal(&m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for key, workflow := range m {
|
||||
workflow.Name = key
|
||||
}
|
||||
|
||||
*wm = m
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Workflow stores a list of steps to execute
|
||||
type Workflow struct {
|
||||
Name string `yaml:"-"`
|
||||
Steps []*Step `yaml:"steps,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalYAML will unmarshal the Workflow from YAML.
|
||||
// The workflow YAML can be specified as either a simple array of steps or a more verbose map/struct style
|
||||
func (w *Workflow) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
parsed := false
|
||||
|
||||
// Map
|
||||
var m map[string]interface{}
|
||||
if err := unmarshal(&m); err == nil {
|
||||
rawName, has := m["name"]
|
||||
if has {
|
||||
w.Name = rawName.(string)
|
||||
}
|
||||
|
||||
rawSteps, has := m["steps"]
|
||||
if has {
|
||||
stepsArray, ok := rawSteps.([]interface{})
|
||||
if ok {
|
||||
w.Steps, err = w.unmarshalSteps(stepsArray)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parsed = true
|
||||
}
|
||||
|
||||
// Array
|
||||
var steps []interface{}
|
||||
if err := unmarshal(&steps); err == nil {
|
||||
w.Steps, err = w.unmarshalSteps(steps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parsed = true
|
||||
}
|
||||
|
||||
if !parsed || len(w.Steps) == 0 {
|
||||
return fmt.Errorf("workflow configuration must be a map or an array of steps")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// unmarshalSteps will unmarshal the steps from YAML.
|
||||
func (w *Workflow) unmarshalSteps(rawSteps any) ([]*Step, error) {
|
||||
stepsArray, ok := rawSteps.([]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("steps must be an array")
|
||||
}
|
||||
|
||||
steps := []*Step{}
|
||||
|
||||
for _, rawStep := range stepsArray {
|
||||
stepYaml, err := yaml.Marshal(rawStep)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var step Step
|
||||
if err := yaml.Unmarshal(stepYaml, &step); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
steps = append(steps, &step)
|
||||
}
|
||||
|
||||
return steps, nil
|
||||
}
|
||||
|
||||
// Step stores a single step to execute within a workflow
|
||||
// This struct can be expanded over time to support other types of steps/commands
|
||||
type Step struct {
|
||||
AzdCommand Command `yaml:"azd,omitempty"`
|
||||
}
|
||||
|
||||
// NewAzdCommandStep creates a new step that executes an azd command with the specified name and args
|
||||
func NewAzdCommandStep(args ...string) *Step {
|
||||
return &Step{
|
||||
AzdCommand: Command{
|
||||
Args: args,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Command stores a single command to execute
|
||||
type Command struct {
|
||||
Args []string `yaml:"args,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalYAML will unmarshal the Command from YAML.
|
||||
// In command YAML the command can be specified as a simple string or a more verbose map/struct style
|
||||
func (c *Command) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
parsed := false
|
||||
|
||||
// Map
|
||||
var m map[string]interface{}
|
||||
if err := unmarshal(&m); err == nil {
|
||||
rawArgs, has := m["args"]
|
||||
if has {
|
||||
argsArray, ok := rawArgs.([]interface{})
|
||||
if ok {
|
||||
for _, arg := range argsArray {
|
||||
argValue, ok := arg.(string)
|
||||
if ok {
|
||||
c.Args = append(c.Args, argValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parsed = true
|
||||
}
|
||||
|
||||
// String
|
||||
var s string
|
||||
if err := unmarshal(&s); err == nil {
|
||||
parts := strings.Split(s, " ")
|
||||
if len(parts) > 0 {
|
||||
c.Args = parts
|
||||
}
|
||||
|
||||
parsed = true
|
||||
}
|
||||
|
||||
if !parsed {
|
||||
return fmt.Errorf("command must be a string or a map")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package environment
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// DataStore is the interface for the interacting with the persistent storage of environments.
|
||||
|
||||
type RemoteKind string
|
||||
|
||||
const (
|
||||
RemoteKindAzureBlobStorage RemoteKind = "AzureBlobStorage"
|
||||
)
|
||||
|
||||
var ValidRemoteKinds = []string{
|
||||
string(RemoteKindAzureBlobStorage),
|
||||
}
|
||||
|
||||
// SaveOptions provide additional metadata for the save operation
|
||||
type SaveOptions struct {
|
||||
// Whether or not the environment is new
|
||||
IsNew bool
|
||||
}
|
||||
|
||||
type DataStore interface {
|
||||
// Gets the path to the environment .env file
|
||||
EnvPath(env *Environment) string
|
||||
|
||||
// Gets the path to the environment JSON config file
|
||||
ConfigPath(env *Environment) string
|
||||
|
||||
// Gets a list of all environments within the stat store
|
||||
List(ctx context.Context) ([]*EnvironmentListItem, error)
|
||||
|
||||
// Gets the environment instance for the specified environment name
|
||||
Get(ctx context.Context, name string) (*Environment, error)
|
||||
|
||||
// Reloads the environment from the persistent data store
|
||||
Reload(ctx context.Context, env *Environment) error
|
||||
|
||||
// Saves the environment to the persistent data store
|
||||
Save(ctx context.Context, env *Environment, options *SaveOptions) error
|
||||
|
||||
// Deletes the environment from the persistent data store
|
||||
Delete(ctx context.Context, name string) error
|
||||
}
|
||||
|
||||
type LocalDataStore DataStore
|
||||
type RemoteDataStore DataStore
|
|
@ -0,0 +1,290 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package environment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"maps"
|
||||
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/config"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
// EnvNameEnvVarName is the name of the key used to store the envname property in the environment.
|
||||
const EnvNameEnvVarName = "AZURE_ENV_NAME"
|
||||
|
||||
// LocationEnvVarName is the name of the key used to store the location property in the environment.
|
||||
const LocationEnvVarName = "AZURE_LOCATION"
|
||||
|
||||
// SubscriptionIdEnvVarName is the name of they key used to store the subscription id property in the environment.
|
||||
const SubscriptionIdEnvVarName = "AZURE_SUBSCRIPTION_ID"
|
||||
|
||||
// PrincipalIdEnvVarName is the name of they key used to store the id of a principal in the environment.
|
||||
const PrincipalIdEnvVarName = "AZURE_PRINCIPAL_ID"
|
||||
|
||||
// TenantIdEnvVarName is the tenant that owns the subscription
|
||||
const TenantIdEnvVarName = "AZURE_TENANT_ID"
|
||||
|
||||
// ContainerRegistryEndpointEnvVarName is the name of they key used to store the endpoint of the container registry to push
|
||||
// to.
|
||||
const ContainerRegistryEndpointEnvVarName = "AZURE_CONTAINER_REGISTRY_ENDPOINT"
|
||||
|
||||
// AksClusterEnvVarName is the name of they key used to store the endpoint of the AKS cluster to push to.
|
||||
const AksClusterEnvVarName = "AZURE_AKS_CLUSTER_NAME"
|
||||
|
||||
// ResourceGroupEnvVarName is the name of the azure resource group that should be used for deployments
|
||||
const ResourceGroupEnvVarName = "AZURE_RESOURCE_GROUP"
|
||||
|
||||
// PlatformTypeEnvVarName is the name of the key used to store the current azd platform type
|
||||
const PlatformTypeEnvVarName = "AZD_PLATFORM_TYPE"
|
||||
|
||||
// The zero value of an Environment is not valid. Use [New] to create one. When writing tests,
|
||||
// [Ephemeral] and [EphemeralWithValues] are useful to create environments which are not persisted to disk.
|
||||
type Environment struct {
|
||||
name string
|
||||
|
||||
// dotenv is a map of keys to values, persisted to the `.env` file stored in this environment's [Root].
|
||||
dotenv map[string]string
|
||||
|
||||
// deletedKeys keeps track of deleted keys from the `.env` to be reapplied before a merge operation
|
||||
// happens in Save
|
||||
deletedKeys map[string]struct{}
|
||||
|
||||
// Config is environment specific config
|
||||
Config config.Config
|
||||
}
|
||||
|
||||
const AzdInitialEnvironmentConfigName = "AZD_INITIAL_ENVIRONMENT_CONFIG"
|
||||
|
||||
// New returns a new environment with the specified name.
|
||||
func New(name string) *Environment {
|
||||
env := &Environment{
|
||||
name: name,
|
||||
dotenv: make(map[string]string),
|
||||
deletedKeys: make(map[string]struct{}),
|
||||
Config: getInitialConfig(),
|
||||
}
|
||||
|
||||
env.DotenvSet(EnvNameEnvVarName, name)
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
func getInitialConfig() config.Config {
|
||||
initialConfig := os.Getenv(AzdInitialEnvironmentConfigName)
|
||||
if initialConfig == "" {
|
||||
return config.NewEmptyConfig()
|
||||
}
|
||||
|
||||
var initConfig map[string]any
|
||||
if err := json.Unmarshal([]byte(initialConfig), &initConfig); err != nil {
|
||||
log.Println("Failed to unmarshal initial config", err, "Using empty config.")
|
||||
return config.NewEmptyConfig()
|
||||
}
|
||||
|
||||
return config.NewConfig(initConfig)
|
||||
}
|
||||
|
||||
// NewWithValues returns an ephemeral environment (i.e. not backed by a data store) with a set
|
||||
// of values. Useful for testing. The name parameter is added to the environment with the
|
||||
// AZURE_ENV_NAME key, replacing an existing value in the provided values map. A nil values is
|
||||
// treated the same way as an empty map.
|
||||
func NewWithValues(name string, values map[string]string) *Environment {
|
||||
env := New(name)
|
||||
|
||||
if values != nil {
|
||||
env.dotenv = values
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
type EnvironmentResolver func(ctx context.Context) (*Environment, error)
|
||||
|
||||
// Same restrictions as a deployment name (ref:
|
||||
// https://docs.microsoft.com/azure/azure-resource-manager/management/resource-name-rules#microsoftresources)
|
||||
var EnvironmentNameRegexp = regexp.MustCompile(`^[a-zA-Z0-9-\(\)_\.]{1,64}$`)
|
||||
|
||||
// The maximum length of an environment name.
|
||||
var EnvironmentNameMaxLength = 64
|
||||
|
||||
func IsValidEnvironmentName(name string) bool {
|
||||
return EnvironmentNameRegexp.MatchString(name)
|
||||
}
|
||||
|
||||
// CleanName returns a version of [name] where all characters not allowed in an environment name have been replaced
|
||||
// with hyphens
|
||||
func CleanName(name string) string {
|
||||
result := strings.Builder{}
|
||||
|
||||
for _, c := range name {
|
||||
if (c >= 'a' && c <= 'z') ||
|
||||
(c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') ||
|
||||
c == '-' ||
|
||||
c == '(' ||
|
||||
c == ')' ||
|
||||
c == '_' ||
|
||||
c == '.' {
|
||||
result.WriteRune(c)
|
||||
} else {
|
||||
result.WriteRune('-')
|
||||
}
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// Getenv behaves like os.Getenv, except that any keys in the `.env` file associated with this environment are considered
|
||||
// first.
|
||||
func (e *Environment) Getenv(key string) string {
|
||||
if v, has := e.dotenv[key]; has {
|
||||
return v
|
||||
}
|
||||
|
||||
return os.Getenv(key)
|
||||
}
|
||||
|
||||
// LookupEnv behaves like os.LookupEnv, except that any keys in the `.env` file associated with this environment are
|
||||
// considered first.
|
||||
func (e *Environment) LookupEnv(key string) (string, bool) {
|
||||
if v, has := e.dotenv[key]; has {
|
||||
return v, true
|
||||
}
|
||||
|
||||
return os.LookupEnv(key)
|
||||
}
|
||||
|
||||
// DotenvDelete removes the given key from the .env file in the environment, it is a no-op if the key
|
||||
// does not exist. [Save] should be called to ensure this change is persisted.
|
||||
func (e *Environment) DotenvDelete(key string) {
|
||||
delete(e.dotenv, key)
|
||||
e.deletedKeys[key] = struct{}{}
|
||||
}
|
||||
|
||||
// Dotenv returns a copy of the key value pairs from the .env file in the environment.
|
||||
func (e *Environment) Dotenv() map[string]string {
|
||||
return maps.Clone(e.dotenv)
|
||||
}
|
||||
|
||||
// DotenvSet sets the value of [key] to [value] in the .env file associated with the environment. [Save] should be
|
||||
// called to ensure this change is persisted.
|
||||
func (e *Environment) DotenvSet(key string, value string) {
|
||||
e.dotenv[key] = value
|
||||
delete(e.deletedKeys, key)
|
||||
}
|
||||
|
||||
// Name gets the name of the environment
|
||||
// If empty will fallback to the value of the AZURE_ENV_NAME environment variable
|
||||
func (e *Environment) Name() string {
|
||||
if e.name == "" {
|
||||
e.name = e.Getenv(EnvNameEnvVarName)
|
||||
}
|
||||
|
||||
return e.name
|
||||
}
|
||||
|
||||
// GetSubscriptionId is shorthand for Getenv(SubscriptionIdEnvVarName)
|
||||
func (e *Environment) GetSubscriptionId() string {
|
||||
return e.Getenv(SubscriptionIdEnvVarName)
|
||||
}
|
||||
|
||||
// GetTenantId is shorthand for Getenv(TenantIdEnvVarName)
|
||||
func (e *Environment) GetTenantId() string {
|
||||
return e.Getenv(TenantIdEnvVarName)
|
||||
}
|
||||
|
||||
// SetLocation is shorthand for DotenvSet(SubscriptionIdEnvVarName, location)
|
||||
func (e *Environment) SetSubscriptionId(id string) {
|
||||
e.DotenvSet(SubscriptionIdEnvVarName, id)
|
||||
}
|
||||
|
||||
// GetLocation is shorthand for Getenv(LocationEnvVarName)
|
||||
func (e *Environment) GetLocation() string {
|
||||
return e.Getenv(LocationEnvVarName)
|
||||
}
|
||||
|
||||
// SetLocation is shorthand for DotenvSet(LocationEnvVarName, location)
|
||||
func (e *Environment) SetLocation(location string) {
|
||||
e.DotenvSet(LocationEnvVarName, location)
|
||||
}
|
||||
|
||||
func normalize(key string) string {
|
||||
return strings.ReplaceAll(strings.ToUpper(key), "-", "_")
|
||||
}
|
||||
|
||||
// GetServiceProperty is shorthand for Getenv(SERVICE_$SERVICE_NAME_$PROPERTY_NAME)
|
||||
func (e *Environment) GetServiceProperty(serviceName string, propertyName string) string {
|
||||
return e.Getenv(fmt.Sprintf("SERVICE_%s_%s", normalize(serviceName), propertyName))
|
||||
}
|
||||
|
||||
// Sets the value of a service-namespaced property in the environment.
|
||||
func (e *Environment) SetServiceProperty(serviceName string, propertyName string, value string) {
|
||||
e.DotenvSet(fmt.Sprintf("SERVICE_%s_%s", normalize(serviceName), propertyName), value)
|
||||
}
|
||||
|
||||
// Creates a slice of key value pairs, based on the entries in the `.env` file like `KEY=VALUE` that
|
||||
// can be used to pass into command runner or similar constructs.
|
||||
func (e *Environment) Environ() []string {
|
||||
envVars := []string{}
|
||||
for k, v := range e.dotenv {
|
||||
envVars = append(envVars, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
|
||||
return envVars
|
||||
}
|
||||
|
||||
// fixupUnquotedDotenv is a workaround for behavior in how godotenv.Marshal handles numeric like values. Marshaling
|
||||
// a map[string]string to a dotenv file, if a value can be successfully parsed with strconv.Atoi, it will be written in
|
||||
// the dotenv file without quotes and the value written will be the value returned by strconv.Atoi. This can lead to dropping
|
||||
// leading zeros from the value that we persist.
|
||||
//
|
||||
// For example, given a map with the key value pair ("FOO", "01"), the value returned by godotenv.Marshal will have a line
|
||||
// that looks like FOO=1 instead of FOO=01 or FOO="01".
|
||||
//
|
||||
// This function takes the value returned by godotenv.Marshal and for any unquoted value replaces it with the value from
|
||||
// the values map if they differ. This means that a key value pair ("FOO", "1") remains as FOO=1.
|
||||
//
|
||||
// When replacing a key in this manner, we ensure the value is wrapped in quotes, on the assumption that the leading zero
|
||||
// is of significance to the value and wrapping it quotes means it is more likely to be treated as a string instead of a
|
||||
// number by any downstream systems. We do not need to worry about escaping quotes in the value, because we know that
|
||||
// godotenv.Marshal only did this translation for numeric values and so we know the original value did not contain quotes.
|
||||
func fixupUnquotedDotenv(values map[string]string, dotenv string) string {
|
||||
entries := strings.Split(dotenv, "\n")
|
||||
for idx, line := range entries {
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
envKey := parts[0]
|
||||
envValue := parts[1]
|
||||
|
||||
if len(envValue) > 0 && envValue[0] != '"' {
|
||||
if values[envKey] != envValue {
|
||||
entries[idx] = fmt.Sprintf("%s=\"%s\"", envKey, values[envKey])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(entries, "\n")
|
||||
}
|
||||
|
||||
// Prepare dotenv for saving and returns a marshalled string that can be save to the underlying data store
|
||||
// Instead of calling `godotenv.Write` directly, we need to save the file ourselves, so we can fixup any numeric values
|
||||
// that were incorrectly unquoted.
|
||||
func marshallDotEnv(env *Environment) (string, error) {
|
||||
marshalled, err := godotenv.Marshal(env.dotenv)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("marshalling .env: %w", err)
|
||||
}
|
||||
|
||||
return fixupUnquotedDotenv(env.dotenv, marshalled), nil
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package environment
|
||||
|
||||
import "fmt"
|
||||
|
||||
type EnvironmentInitError struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func NewEnvironmentInitError(envName string) *EnvironmentInitError {
|
||||
return &EnvironmentInitError{Name: envName}
|
||||
}
|
||||
|
||||
func (err *EnvironmentInitError) Error() string {
|
||||
return fmt.Sprintf("environment already initialized to %s", err.Name)
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
package environment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/config"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
// LocalFileDataStore is a DataStore implementation that stores environment data in the local file system.
|
||||
type LocalFileDataStore struct {
|
||||
azdContext *azdcore.Context
|
||||
configManager config.FileConfigManager
|
||||
}
|
||||
|
||||
// NewLocalFileDataStore creates a new LocalFileDataStore instance
|
||||
func NewLocalFileDataStore(azdContext *azdcore.Context, configManager config.FileConfigManager) LocalDataStore {
|
||||
return &LocalFileDataStore{
|
||||
azdContext: azdContext,
|
||||
configManager: configManager,
|
||||
}
|
||||
}
|
||||
|
||||
// Path returns the path to the .env file for the given environment
|
||||
func (fs *LocalFileDataStore) EnvPath(env *Environment) string {
|
||||
return filepath.Join(fs.azdContext.EnvironmentRoot(env.name), DotEnvFileName)
|
||||
}
|
||||
|
||||
// ConfigPath returns the path to the config.json file for the given environment
|
||||
func (fs *LocalFileDataStore) ConfigPath(env *Environment) string {
|
||||
return filepath.Join(fs.azdContext.EnvironmentRoot(env.name), ConfigFileName)
|
||||
}
|
||||
|
||||
// List returns a list of all environments within the data store
|
||||
func (fs *LocalFileDataStore) List(ctx context.Context) ([]*EnvironmentListItem, error) {
|
||||
defaultEnv, err := fs.azdContext.GetDefaultEnvironmentName()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
environments, err := os.ReadDir(fs.azdContext.EnvironmentDirectory())
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return []*EnvironmentListItem{}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listing entries: %w", err)
|
||||
}
|
||||
|
||||
// prefer empty array over `nil` since this is a contracted return value,
|
||||
// where empty array is preferred for "NotFound" semantics.
|
||||
envs := []*EnvironmentListItem{}
|
||||
for _, ent := range environments {
|
||||
if ent.IsDir() {
|
||||
ev := &EnvironmentListItem{
|
||||
Name: ent.Name(),
|
||||
IsDefault: ent.Name() == defaultEnv,
|
||||
DotEnvPath: filepath.Join(fs.azdContext.EnvironmentRoot(ent.Name()), DotEnvFileName),
|
||||
ConfigPath: filepath.Join(fs.azdContext.EnvironmentRoot(ent.Name()), ConfigFileName),
|
||||
}
|
||||
envs = append(envs, ev)
|
||||
}
|
||||
}
|
||||
|
||||
slices.SortFunc(envs, func(a, b *EnvironmentListItem) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
return envs, nil
|
||||
}
|
||||
|
||||
// Get returns the environment instance for the specified environment name
|
||||
func (fs *LocalFileDataStore) Get(ctx context.Context, name string) (*Environment, error) {
|
||||
root := fs.azdContext.EnvironmentRoot(name)
|
||||
_, err := os.Stat(root)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, fmt.Errorf("'%s': %w", name, ErrNotFound)
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("listing env root: %w", err)
|
||||
}
|
||||
|
||||
env := New(name)
|
||||
if err := fs.Reload(ctx, env); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return env, nil
|
||||
}
|
||||
|
||||
// Reload reloads the environment from the persistent data store
|
||||
func (fs *LocalFileDataStore) Reload(ctx context.Context, env *Environment) error {
|
||||
// Reload env values
|
||||
if envMap, err := godotenv.Read(fs.EnvPath(env)); errors.Is(err, os.ErrNotExist) {
|
||||
env.dotenv = make(map[string]string)
|
||||
env.deletedKeys = make(map[string]struct{})
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("loading .env: %w", err)
|
||||
} else {
|
||||
env.dotenv = envMap
|
||||
env.deletedKeys = make(map[string]struct{})
|
||||
}
|
||||
|
||||
// Reload env config
|
||||
if cfg, err := fs.configManager.Load(fs.ConfigPath(env)); errors.Is(err, os.ErrNotExist) {
|
||||
env.Config = config.NewEmptyConfig()
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("loading config: %w", err)
|
||||
} else {
|
||||
env.Config = cfg
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save saves the environment to the persistent data store
|
||||
func (fs *LocalFileDataStore) Save(ctx context.Context, env *Environment, options *SaveOptions) error {
|
||||
// Update configuration
|
||||
if err := fs.configManager.Save(env.Config, fs.ConfigPath(env)); err != nil {
|
||||
return fmt.Errorf("saving config: %w", err)
|
||||
}
|
||||
|
||||
// Cache current values & reload to get any new env vars
|
||||
currentValues := env.dotenv
|
||||
deletedValues := env.deletedKeys
|
||||
if err := fs.Reload(ctx, env); err != nil {
|
||||
return fmt.Errorf("failed reloading env vars, %w", err)
|
||||
}
|
||||
|
||||
// Overlay current values before saving
|
||||
for key, value := range currentValues {
|
||||
env.dotenv[key] = value
|
||||
}
|
||||
|
||||
// Replay deletion
|
||||
for key := range deletedValues {
|
||||
delete(env.dotenv, key)
|
||||
}
|
||||
|
||||
marshalled, err := marshallDotEnv(env)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshalling .env: %w", err)
|
||||
}
|
||||
|
||||
envFile, err := os.Create(fs.EnvPath(env))
|
||||
if err != nil {
|
||||
return fmt.Errorf("saving .env: %w", err)
|
||||
}
|
||||
defer envFile.Close()
|
||||
|
||||
// Write the contents (with a trailing newline), and sync the file, as godotenv.Write would have.
|
||||
if _, err := envFile.WriteString(marshalled + "\n"); err != nil {
|
||||
return fmt.Errorf("saving .env: %w", err)
|
||||
}
|
||||
|
||||
if err := envFile.Sync(); err != nil {
|
||||
return fmt.Errorf("saving .env: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *LocalFileDataStore) Delete(ctx context.Context, name string) error {
|
||||
envRoot := fs.azdContext.EnvironmentRoot(name)
|
||||
_, err := os.Stat(envRoot)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("'%s': %w", name, ErrNotFound)
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("listing env root: %w", err)
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(envRoot); err != nil {
|
||||
return fmt.Errorf("removing env root: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,331 @@
|
|||
package environment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/common/ioc"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/contracts"
|
||||
)
|
||||
|
||||
// Description is a metadata description of an environment returned for the `azd env list` command
|
||||
type Description struct {
|
||||
// The name of the environment
|
||||
Name string
|
||||
// The path to the local .env file for the environment. Useful for IDEs like VS / VSCode
|
||||
DotEnvPath string
|
||||
// Specifies when the environment exists locally
|
||||
HasLocal bool
|
||||
// Specifies when the environment exists remotely
|
||||
HasRemote bool
|
||||
// Specifies when the environment is the default environment
|
||||
IsDefault bool
|
||||
}
|
||||
|
||||
// Spec is the specification for creating a new environment
|
||||
type Spec struct {
|
||||
Name string
|
||||
Subscription string
|
||||
Location string
|
||||
// suggest is the name that is offered as a suggestion if we need to prompt the user for an environment name.
|
||||
Examples []string
|
||||
}
|
||||
|
||||
const DotEnvFileName = ".env"
|
||||
const ConfigFileName = "config.json"
|
||||
|
||||
var (
|
||||
// Error returned when an environment with the specified name already exists
|
||||
ErrExists = errors.New("environment already exists")
|
||||
|
||||
// Error returned when an environment with a specified name cannot be found
|
||||
ErrNotFound = errors.New("environment not found")
|
||||
|
||||
// Error returned when an environment name is not specified
|
||||
ErrNameNotSpecified = errors.New("environment not specified")
|
||||
)
|
||||
|
||||
// Manager is the interface used for managing instances of environments
|
||||
type Manager interface {
|
||||
Create(ctx context.Context, spec Spec) (*Environment, error)
|
||||
|
||||
List(ctx context.Context) ([]*Description, error)
|
||||
|
||||
// Get returns the existing environment with the given name.
|
||||
// If the environment specified by the given name does not exist, ErrNotFound is returned.
|
||||
Get(ctx context.Context, name string) (*Environment, error)
|
||||
|
||||
Save(ctx context.Context, env *Environment) error
|
||||
SaveWithOptions(ctx context.Context, env *Environment, options *SaveOptions) error
|
||||
Reload(ctx context.Context, env *Environment) error
|
||||
|
||||
// Delete deletes the environment from local storage.
|
||||
Delete(ctx context.Context, name string) error
|
||||
|
||||
EnvPath(env *Environment) string
|
||||
ConfigPath(env *Environment) string
|
||||
}
|
||||
|
||||
type manager struct {
|
||||
local DataStore
|
||||
remote DataStore
|
||||
azdContext *azdcore.Context
|
||||
}
|
||||
|
||||
// NewManager creates a new Manager instance
|
||||
func NewManager(
|
||||
serviceLocator ioc.ServiceLocator,
|
||||
azdContext *azdcore.Context,
|
||||
local LocalDataStore,
|
||||
remoteConfig *contracts.RemoteConfig,
|
||||
) (Manager, error) {
|
||||
var remote RemoteDataStore
|
||||
|
||||
// Ideally we would have liked to inject the remote data store directly into the manager,
|
||||
// via the container but we can't do that because the remote data store is optional and the IoC
|
||||
// container doesn't support optional interface based dependencies.
|
||||
if remoteConfig != nil {
|
||||
err := serviceLocator.ResolveNamed(remoteConfig.Backend, &remote)
|
||||
if err != nil {
|
||||
if errors.Is(err, ioc.ErrResolveInstance) {
|
||||
return nil, fmt.Errorf(
|
||||
"remote state configuration is invalid. The specified backend '%s' is not valid. Valid values are '%s'.",
|
||||
remoteConfig.Backend,
|
||||
strings.Join(ValidRemoteKinds, ","),
|
||||
)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("resolving remote state data store: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &manager{
|
||||
azdContext: azdContext,
|
||||
local: local,
|
||||
remote: remote,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *manager) Create(ctx context.Context, spec Spec) (*Environment, error) {
|
||||
if spec.Name != "" && !IsValidEnvironmentName(spec.Name) {
|
||||
errMsg := invalidEnvironmentNameMsg(spec.Name)
|
||||
return nil, errors.New(errMsg)
|
||||
}
|
||||
|
||||
if err := m.ensureValidEnvironmentName(ctx, &spec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ensure the environment does not already exist:
|
||||
_, err := m.Get(ctx, spec.Name)
|
||||
switch {
|
||||
case errors.Is(err, ErrNotFound):
|
||||
case err != nil:
|
||||
return nil, fmt.Errorf("checking for existing environment: %w", err)
|
||||
default:
|
||||
return nil, fmt.Errorf("environment '%s' already exists", spec.Name)
|
||||
}
|
||||
|
||||
env := New(spec.Name)
|
||||
|
||||
if spec.Subscription != "" {
|
||||
env.SetSubscriptionId(spec.Subscription)
|
||||
}
|
||||
|
||||
if spec.Location != "" {
|
||||
env.SetLocation(spec.Location)
|
||||
}
|
||||
|
||||
if err := m.SaveWithOptions(ctx, env, &SaveOptions{IsNew: true}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return env, nil
|
||||
}
|
||||
|
||||
// ConfigPath returns the path to the environment config file
|
||||
func (m *manager) ConfigPath(env *Environment) string {
|
||||
return m.local.ConfigPath(env)
|
||||
}
|
||||
|
||||
// EnvPath returns the path to the environment .env file
|
||||
func (m *manager) EnvPath(env *Environment) string {
|
||||
return m.local.EnvPath(env)
|
||||
}
|
||||
|
||||
// List returns a list of all environments within the data store
|
||||
func (m *manager) List(ctx context.Context) ([]*Description, error) {
|
||||
envMap := map[string]*Description{}
|
||||
defaultEnvName, err := m.azdContext.GetDefaultEnvironmentName()
|
||||
if err != nil {
|
||||
defaultEnvName = ""
|
||||
}
|
||||
|
||||
localEnvs, err := m.local.List(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("retrieving local environments, %w", err)
|
||||
}
|
||||
|
||||
for _, env := range localEnvs {
|
||||
envMap[env.Name] = &Description{
|
||||
Name: env.Name,
|
||||
HasLocal: true,
|
||||
DotEnvPath: env.DotEnvPath,
|
||||
}
|
||||
}
|
||||
|
||||
if m.remote != nil {
|
||||
remoteEnvs, err := m.remote.List(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("retrieving remote environments, %w", err)
|
||||
}
|
||||
|
||||
for _, env := range remoteEnvs {
|
||||
existing, has := envMap[env.Name]
|
||||
if !has {
|
||||
existing = &Description{
|
||||
Name: env.Name,
|
||||
HasRemote: true,
|
||||
}
|
||||
} else {
|
||||
existing.HasRemote = true
|
||||
}
|
||||
envMap[env.Name] = existing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
allEnvs := []*Description{}
|
||||
for _, env := range envMap {
|
||||
env.IsDefault = env.Name == defaultEnvName
|
||||
allEnvs = append(allEnvs, env)
|
||||
}
|
||||
|
||||
slices.SortFunc(allEnvs, func(a, b *Description) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
return allEnvs, nil
|
||||
}
|
||||
|
||||
// Get returns the environment instance for the specified environment name
|
||||
func (m *manager) Get(ctx context.Context, name string) (*Environment, error) {
|
||||
if name == "" {
|
||||
return nil, ErrNameNotSpecified
|
||||
}
|
||||
|
||||
localEnv, err := m.local.Get(ctx, name)
|
||||
if err != nil {
|
||||
if m.remote == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
remoteEnv, err := m.remote.Get(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := m.local.Save(ctx, remoteEnv, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
localEnv = remoteEnv
|
||||
}
|
||||
|
||||
// Ensures local environment variable name is synced with the environment name
|
||||
envName, ok := localEnv.LookupEnv(EnvNameEnvVarName)
|
||||
if !ok || envName != name {
|
||||
localEnv.DotenvSet(EnvNameEnvVarName, name)
|
||||
if err := m.Save(ctx, localEnv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return localEnv, nil
|
||||
}
|
||||
|
||||
// Save saves the environment to the persistent data store
|
||||
func (m *manager) Save(ctx context.Context, env *Environment) error {
|
||||
return m.SaveWithOptions(ctx, env, nil)
|
||||
}
|
||||
|
||||
// Save saves the environment to the persistent data store with the specified options
|
||||
func (m *manager) SaveWithOptions(ctx context.Context, env *Environment, options *SaveOptions) error {
|
||||
if options == nil {
|
||||
options = &SaveOptions{}
|
||||
}
|
||||
|
||||
if err := m.local.Save(ctx, env, options); err != nil {
|
||||
return fmt.Errorf("saving local environment, %w", err)
|
||||
}
|
||||
|
||||
if m.remote == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := m.remote.Save(ctx, env, options); err != nil {
|
||||
return fmt.Errorf("saving remote environment, %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reload reloads the environment from the persistent data store
|
||||
func (m *manager) Reload(ctx context.Context, env *Environment) error {
|
||||
return m.local.Reload(ctx, env)
|
||||
}
|
||||
|
||||
func (m *manager) Delete(ctx context.Context, name string) error {
|
||||
if name == "" {
|
||||
return ErrNameNotSpecified
|
||||
}
|
||||
|
||||
err := m.local.Delete(ctx, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defaultEnvName, err := m.azdContext.GetDefaultEnvironmentName()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting default environment: %w", err)
|
||||
}
|
||||
|
||||
if defaultEnvName == name {
|
||||
err = m.azdContext.SetProjectState(azdcore.ProjectState{DefaultEnvironment: ""})
|
||||
if err != nil {
|
||||
return fmt.Errorf("clearing default environment: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureValidEnvironmentName ensures the environment name is valid, if it is not, an error is printed
|
||||
// and the user is prompted for a new name.
|
||||
func (m *manager) ensureValidEnvironmentName(ctx context.Context, spec *Spec) error {
|
||||
exampleText := ""
|
||||
if len(spec.Examples) > 0 {
|
||||
exampleText = "\n\nExamples:"
|
||||
}
|
||||
|
||||
for _, example := range spec.Examples {
|
||||
exampleText += fmt.Sprintf("\n %s", example)
|
||||
}
|
||||
|
||||
for !IsValidEnvironmentName(spec.Name) {
|
||||
return errors.New(invalidEnvironmentNameMsg(spec.Name))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func invalidEnvironmentNameMsg(environmentName string) string {
|
||||
return fmt.Sprintf(
|
||||
"environment name '%s' is invalid (it should contain only alphanumeric characters and hyphens)\n",
|
||||
environmentName,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package environment
|
||||
|
||||
type EnvironmentListItem struct {
|
||||
Name string `json:"Name"`
|
||||
IsDefault bool `json:"IsDefault"`
|
||||
DotEnvPath string `json:"DotEnvPath"`
|
||||
ConfigPath string `json:"ConfigPath"`
|
||||
}
|
|
@ -0,0 +1,241 @@
|
|||
package environment
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/azure/storage"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/config"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/internal/tracing"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/internal/tracing/fields"
|
||||
"github.com/google/uuid"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrAccessDenied = errors.New("access denied connecting Azure Blob Storage container.")
|
||||
ErrInvalidContainer = errors.New("storage container name is invalid.")
|
||||
)
|
||||
|
||||
type StorageBlobDataStore struct {
|
||||
configManager config.Manager
|
||||
blobClient storage.BlobClient
|
||||
}
|
||||
|
||||
func NewStorageBlobDataStore(configManager config.Manager, blobClient storage.BlobClient) RemoteDataStore {
|
||||
return &StorageBlobDataStore{
|
||||
configManager: configManager,
|
||||
blobClient: blobClient,
|
||||
}
|
||||
}
|
||||
|
||||
// EnvPath returns the path to the .env file for the given environment
|
||||
func (fs *StorageBlobDataStore) EnvPath(env *Environment) string {
|
||||
return fmt.Sprintf("%s/%s", env.name, DotEnvFileName)
|
||||
}
|
||||
|
||||
// ConfigPath returns the path to the config.json file for the given environment
|
||||
func (fs *StorageBlobDataStore) ConfigPath(env *Environment) string {
|
||||
return fmt.Sprintf("%s/%s", env.name, ConfigFileName)
|
||||
}
|
||||
|
||||
func (sbd *StorageBlobDataStore) List(ctx context.Context) ([]*EnvironmentListItem, error) {
|
||||
blobs, err := sbd.blobClient.Items(ctx)
|
||||
if err != nil {
|
||||
normalizedErr := describeError(err)
|
||||
|
||||
if errors.Is(normalizedErr, storage.ErrContainerNotFound) {
|
||||
return []*EnvironmentListItem{}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("listing blobs: %w", normalizedErr)
|
||||
}
|
||||
|
||||
envMap := map[string]*EnvironmentListItem{}
|
||||
|
||||
for _, blob := range blobs {
|
||||
envName := filepath.Base(filepath.Dir(blob.Path))
|
||||
env, has := envMap[envName]
|
||||
if !has {
|
||||
env = &EnvironmentListItem{
|
||||
Name: envName,
|
||||
}
|
||||
envMap[envName] = env
|
||||
}
|
||||
|
||||
switch blob.Name {
|
||||
case ConfigFileName:
|
||||
env.ConfigPath = blob.Path
|
||||
case DotEnvFileName:
|
||||
env.DotEnvPath = blob.Path
|
||||
}
|
||||
}
|
||||
|
||||
envs := []*EnvironmentListItem{}
|
||||
for _, env := range envMap {
|
||||
envs = append(envs, env)
|
||||
}
|
||||
|
||||
slices.SortFunc(envs, func(a, b *EnvironmentListItem) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
return envs, nil
|
||||
}
|
||||
|
||||
func (sbd *StorageBlobDataStore) Get(ctx context.Context, name string) (*Environment, error) {
|
||||
envs, err := sbd.List(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
matchingIndex := slices.IndexFunc(envs, func(env *EnvironmentListItem) bool {
|
||||
return env.Name == name
|
||||
})
|
||||
|
||||
if matchingIndex < 0 {
|
||||
return nil, fmt.Errorf("'%s': %w", name, ErrNotFound)
|
||||
}
|
||||
|
||||
matchingEnv := envs[matchingIndex]
|
||||
env := &Environment{
|
||||
name: matchingEnv.Name,
|
||||
}
|
||||
|
||||
if err := sbd.Reload(ctx, env); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return env, nil
|
||||
}
|
||||
|
||||
func (sbd *StorageBlobDataStore) Save(ctx context.Context, env *Environment, options *SaveOptions) error {
|
||||
// Update configuration
|
||||
cfgWriter := new(bytes.Buffer)
|
||||
|
||||
if err := sbd.configManager.Save(env.Config, cfgWriter); err != nil {
|
||||
return fmt.Errorf("saving config: %w", err)
|
||||
}
|
||||
|
||||
if err := sbd.blobClient.Upload(ctx, sbd.ConfigPath(env), cfgWriter); err != nil {
|
||||
return fmt.Errorf("uploading config: %w", describeError(err))
|
||||
}
|
||||
|
||||
marshalled, err := marshallDotEnv(env)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshalling .env: %w", err)
|
||||
}
|
||||
|
||||
buffer := bytes.NewBuffer([]byte(marshalled))
|
||||
|
||||
if err := sbd.blobClient.Upload(ctx, sbd.EnvPath(env), buffer); err != nil {
|
||||
return fmt.Errorf("uploading .env: %w", describeError(err))
|
||||
}
|
||||
|
||||
tracing.SetUsageAttributes(fields.StringHashed(fields.EnvNameKey, env.Name()))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sbd *StorageBlobDataStore) Reload(ctx context.Context, env *Environment) error {
|
||||
// Reload .env file
|
||||
dotEnvBuffer, err := sbd.blobClient.Download(ctx, sbd.EnvPath(env))
|
||||
if err != nil {
|
||||
return describeError(err)
|
||||
}
|
||||
|
||||
defer dotEnvBuffer.Close()
|
||||
|
||||
envMap, err := godotenv.Parse(dotEnvBuffer)
|
||||
if err != nil {
|
||||
env.dotenv = make(map[string]string)
|
||||
env.deletedKeys = make(map[string]struct{})
|
||||
} else {
|
||||
env.dotenv = envMap
|
||||
env.deletedKeys = make(map[string]struct{})
|
||||
}
|
||||
|
||||
// Reload config file
|
||||
configBuffer, err := sbd.blobClient.Download(ctx, sbd.ConfigPath(env))
|
||||
if err != nil {
|
||||
return describeError(err)
|
||||
}
|
||||
|
||||
defer configBuffer.Close()
|
||||
|
||||
if cfg, err := sbd.configManager.Load(configBuffer); errors.Is(err, os.ErrNotExist) {
|
||||
env.Config = config.NewEmptyConfig()
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("loading config: %w", err)
|
||||
} else {
|
||||
env.Config = cfg
|
||||
}
|
||||
|
||||
if env.Name() != "" {
|
||||
tracing.SetUsageAttributes(fields.StringHashed(fields.EnvNameKey, env.Name()))
|
||||
}
|
||||
|
||||
if _, err := uuid.Parse(env.GetSubscriptionId()); err == nil {
|
||||
tracing.SetGlobalAttributes(fields.SubscriptionIdKey.String(env.GetSubscriptionId()))
|
||||
} else {
|
||||
tracing.SetGlobalAttributes(fields.StringHashed(fields.SubscriptionIdKey, env.GetSubscriptionId()))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sbd *StorageBlobDataStore) Delete(ctx context.Context, name string) error {
|
||||
envs, err := sbd.List(ctx)
|
||||
if err != nil {
|
||||
return describeError(err)
|
||||
}
|
||||
|
||||
matchingIndex := slices.IndexFunc(envs, func(env *EnvironmentListItem) bool {
|
||||
return env.Name == name
|
||||
})
|
||||
|
||||
if matchingIndex < 0 {
|
||||
return fmt.Errorf("'%s': %w", name, ErrNotFound)
|
||||
}
|
||||
|
||||
env := envs[matchingIndex]
|
||||
if env.ConfigPath != "" {
|
||||
err := sbd.blobClient.Delete(ctx, env.ConfigPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting remote config: %w", describeError(err))
|
||||
}
|
||||
}
|
||||
|
||||
if env.DotEnvPath != "" {
|
||||
err := sbd.blobClient.Delete(ctx, env.DotEnvPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting remote .env: %w", describeError(err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func describeError(err error) error {
|
||||
var responseErr *azcore.ResponseError
|
||||
|
||||
if errors.As(err, &responseErr) {
|
||||
switch responseErr.ErrorCode {
|
||||
case "AuthorizationPermissionMismatch":
|
||||
errorMsg := "Ensure your Azure account has `Storage Blob Contributor` role on the storage account or container."
|
||||
return fmt.Errorf("%w %s %w", ErrAccessDenied, errorMsg, err)
|
||||
case "InvalidResourceName":
|
||||
//nolint:lll
|
||||
errorMsg := "It must be between 3 and 63 characters in length, and must contain only lowercase letters, numbers, and dashes."
|
||||
return fmt.Errorf("%w %s %w", ErrInvalidContainer, errorMsg, err)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/azure"
|
||||
)
|
||||
|
||||
type Principal azure.TokenClaims
|
|
@ -0,0 +1,216 @@
|
|||
package ext
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/azure"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/common/ioc"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/config"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/contracts"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/environment"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/ext/account"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/project"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrProjectNotFound = errors.New("azd project not found in current path")
|
||||
ErrEnvironmentNotFound = errors.New("azd environment not found")
|
||||
ErrUserConfigNotFound = errors.New("azd user config not found")
|
||||
ErrPrincipalNotFound = errors.New("azd principal not found")
|
||||
ErrNotLoggedIn = errors.New("azd credential not available")
|
||||
)
|
||||
|
||||
var current *Context
|
||||
|
||||
type Context struct {
|
||||
container *ioc.NestedContainer
|
||||
project *project.ProjectConfig
|
||||
environment *environment.Environment
|
||||
userConfig config.UserConfig
|
||||
credential azcore.TokenCredential
|
||||
principal *account.Principal
|
||||
}
|
||||
|
||||
func CurrentContext(ctx context.Context) (*Context, error) {
|
||||
if current == nil {
|
||||
container := ioc.NewNestedContainer(nil)
|
||||
registerComponents(ctx, container)
|
||||
|
||||
current = &Context{
|
||||
container: container,
|
||||
}
|
||||
}
|
||||
|
||||
return current, nil
|
||||
}
|
||||
|
||||
func (c *Context) Project(ctx context.Context) (*project.ProjectConfig, error) {
|
||||
if c.project == nil {
|
||||
var project *project.ProjectConfig
|
||||
if err := c.container.Resolve(&project); err != nil {
|
||||
return nil, fmt.Errorf("%w, Details: %w", ErrProjectNotFound, err)
|
||||
}
|
||||
|
||||
c.project = project
|
||||
}
|
||||
|
||||
return c.project, nil
|
||||
}
|
||||
|
||||
func (c *Context) Environment(ctx context.Context) (*environment.Environment, error) {
|
||||
if c.environment == nil {
|
||||
var env *environment.Environment
|
||||
if err := c.container.Resolve(&env); err != nil {
|
||||
return nil, fmt.Errorf("%w, Details: %w", ErrEnvironmentNotFound, err)
|
||||
}
|
||||
|
||||
c.environment = env
|
||||
}
|
||||
|
||||
return c.environment, nil
|
||||
}
|
||||
|
||||
func (c *Context) UserConfig(ctx context.Context) (config.UserConfig, error) {
|
||||
if c.userConfig == nil {
|
||||
var userConfig config.UserConfig
|
||||
if err := c.container.Resolve(&userConfig); err != nil {
|
||||
return nil, fmt.Errorf("%w, Details: %w", ErrUserConfigNotFound, err)
|
||||
}
|
||||
|
||||
c.userConfig = userConfig
|
||||
}
|
||||
|
||||
return c.userConfig, nil
|
||||
}
|
||||
|
||||
func (c *Context) Credential() (azcore.TokenCredential, error) {
|
||||
if c.credential == nil {
|
||||
azdCredential, err := azidentity.NewAzureDeveloperCLICredential(nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w, Details: %w", ErrNotLoggedIn, err)
|
||||
}
|
||||
|
||||
c.credential = azdCredential
|
||||
}
|
||||
|
||||
return c.credential, nil
|
||||
}
|
||||
|
||||
func (c *Context) Principal(ctx context.Context) (*account.Principal, error) {
|
||||
if c.principal == nil {
|
||||
credential, err := c.Credential()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w, Details: %w", ErrPrincipalNotFound, err)
|
||||
}
|
||||
|
||||
accessToken, err := credential.GetToken(ctx, policy.TokenRequestOptions{
|
||||
Scopes: []string{"https://management.azure.com/.default"},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claims, err := azure.GetClaimsFromAccessToken(accessToken.Token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
principal := account.Principal(claims)
|
||||
c.principal = &principal
|
||||
}
|
||||
|
||||
return c.principal, nil
|
||||
}
|
||||
|
||||
func (c *Context) SaveEnvironment(ctx context.Context, env *environment.Environment) error {
|
||||
err := c.container.Invoke(func(envManager environment.Manager) error {
|
||||
return envManager.Save(ctx, env)
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Context) SaveUserConfig(ctx context.Context, userConfig config.UserConfig) error {
|
||||
err := c.container.Invoke(func(userConfigManager config.UserConfigManager) error {
|
||||
return userConfigManager.Save(userConfig)
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func registerComponents(ctx context.Context, container *ioc.NestedContainer) error {
|
||||
container.MustRegisterSingleton(func() ioc.ServiceLocator {
|
||||
return container
|
||||
})
|
||||
|
||||
container.MustRegisterSingleton(azdcore.NewContext)
|
||||
container.MustRegisterSingleton(environment.NewManager)
|
||||
container.MustRegisterSingleton(environment.NewLocalFileDataStore)
|
||||
container.MustRegisterSingleton(config.NewFileConfigManager)
|
||||
container.MustRegisterSingleton(config.NewManager)
|
||||
container.MustRegisterSingleton(config.NewUserConfigManager)
|
||||
|
||||
container.MustRegisterSingleton(func(azdContext *azdcore.Context) (*project.ProjectConfig, error) {
|
||||
if azdContext == nil {
|
||||
return nil, azdcore.ErrNoProject
|
||||
}
|
||||
|
||||
return project.Load(ctx, azdContext.ProjectPath())
|
||||
})
|
||||
|
||||
container.MustRegisterSingleton(
|
||||
func(azdContext *azdcore.Context, envManager environment.Manager) (*environment.Environment, error) {
|
||||
if azdContext == nil {
|
||||
return nil, azdcore.ErrNoProject
|
||||
}
|
||||
|
||||
envName, err := azdContext.GetDefaultEnvironmentName()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
environment, err := envManager.Get(ctx, envName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return environment, nil
|
||||
},
|
||||
)
|
||||
|
||||
container.MustRegisterSingleton(func(userConfigManager config.UserConfigManager) (config.UserConfig, error) {
|
||||
return userConfigManager.Load()
|
||||
})
|
||||
|
||||
container.MustRegisterSingleton(
|
||||
func(projectConfig *project.ProjectConfig, userConfigManager config.UserConfigManager) (*contracts.RemoteConfig, error) {
|
||||
var remoteStateConfig *contracts.RemoteConfig
|
||||
|
||||
userConfig, err := userConfigManager.Load()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading user config: %w", err)
|
||||
}
|
||||
|
||||
// Lookup remote state config in the following precedence:
|
||||
// 1. Project azure.yaml
|
||||
// 2. User configuration
|
||||
if projectConfig != nil && projectConfig.State != nil && projectConfig.State.Remote != nil {
|
||||
remoteStateConfig = projectConfig.State.Remote
|
||||
} else {
|
||||
if _, err := userConfig.GetSection("state.remote", &remoteStateConfig); err != nil {
|
||||
return nil, fmt.Errorf("getting remote state config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return remoteStateConfig, nil
|
||||
},
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/ux"
|
||||
)
|
||||
|
||||
func WaitForDebugger() {
|
||||
if _, has := os.LookupEnv("AZD_DEBUG"); has {
|
||||
for {
|
||||
debugConfirm := ux.NewConfirm(&ux.ConfirmConfig{
|
||||
Message: fmt.Sprintf("Debugger Ready? (pid: %d)", os.Getpid()),
|
||||
DefaultValue: ux.Ptr(true),
|
||||
})
|
||||
|
||||
ready, err := debugConfirm.Ask()
|
||||
if err == nil && *ready {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
package ext
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
azcorelog "github.com/Azure/azure-sdk-for-go/sdk/azcore/log"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
var logFile *os.File
|
||||
|
||||
func init() {
|
||||
if isDebugEnabled() {
|
||||
azcorelog.SetListener(func(event azcorelog.Event, msg string) {
|
||||
log.Printf("%s: %s\n", event, msg)
|
||||
})
|
||||
} else {
|
||||
var err error
|
||||
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get executable name: %v", err)
|
||||
}
|
||||
|
||||
exeNameWithExt := filepath.Base(exePath) // Get the base name of the executable
|
||||
exeName := strings.TrimSuffix(exeNameWithExt, filepath.Ext(exeNameWithExt)) // Remove the extension
|
||||
|
||||
currentDate := time.Now().Format("20060102") // Format the current date as YYYYMMDD
|
||||
logFileName := exeName + "-" + currentDate + ".log" // Log file name based on the date
|
||||
|
||||
logFile, err = os.OpenFile(logFileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open log file: %v", err)
|
||||
}
|
||||
|
||||
// Set log output to the file
|
||||
log.SetOutput(logFile)
|
||||
|
||||
// Optional: Adds timestamp and file information to each log entry
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
|
||||
// Register the signal handler to ensure log file is closed gracefully
|
||||
setupSignalHandler()
|
||||
}
|
||||
}
|
||||
|
||||
// setupSignalHandler listens for system signals (e.g., SIGINT, SIGTERM) and ensures cleanup
|
||||
func setupSignalHandler() {
|
||||
signals := make(chan os.Signal, 1)
|
||||
// Notify channel on SIGINT (Ctrl+C) or SIGTERM (termination)
|
||||
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
// Goroutine to handle the signals
|
||||
go func() {
|
||||
// Block until we receive a signal
|
||||
sig := <-signals
|
||||
log.Printf("Received signal: %v, shutting down gracefully...", sig)
|
||||
|
||||
// Perform cleanup
|
||||
flush()
|
||||
}()
|
||||
}
|
||||
|
||||
// flush ensures the log file is closed
|
||||
func flush() {
|
||||
if logFile != nil {
|
||||
logFile.Sync()
|
||||
}
|
||||
}
|
||||
|
||||
// isDebugEnabled checks to see if `--debug` was passed with a truthy
|
||||
// value.
|
||||
func isDebugEnabled() bool {
|
||||
debug := false
|
||||
flags := pflag.NewFlagSet("", pflag.ContinueOnError)
|
||||
|
||||
// Since we are running this parse logic on the full command line, there may be additional flags
|
||||
// which we have not defined in our flag set (but would be defined by whatever command we end up
|
||||
// running). Setting UnknownFlags instructs `flags.Parse` to continue parsing the command line
|
||||
// even if a flag is not in the flag set (instead of just returning an error saying the flag was not
|
||||
// found).
|
||||
flags.ParseErrorsWhitelist.UnknownFlags = true
|
||||
flags.BoolVar(&debug, "debug", false, "")
|
||||
|
||||
// if flag `-h` of `--help` is within the command, the usage is automatically shown.
|
||||
// Setting `Usage` to a no-op will hide this extra unwanted output.
|
||||
flags.Usage = func() {}
|
||||
|
||||
_ = flags.Parse(os.Args[1:])
|
||||
return debug
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package ext
|
||||
|
||||
type ErrorWithSuggestion struct {
|
||||
Err error
|
||||
Suggestion string
|
||||
}
|
||||
|
||||
func (e *ErrorWithSuggestion) Error() string {
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
func (e *ErrorWithSuggestion) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/ux"
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
type CommandHeader struct {
|
||||
Title string
|
||||
Description string
|
||||
}
|
||||
|
||||
func (ch CommandHeader) Print() {
|
||||
color.White(ux.BoldString(ch.Title))
|
||||
if ch.Description != "" {
|
||||
color.HiBlack(ch.Description)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
|
@ -0,0 +1,530 @@
|
|||
package prompt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"dario.cat/mergo"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/azure"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/ext"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/ux"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoResourcesFound = fmt.Errorf("no resources found")
|
||||
)
|
||||
|
||||
type PromptResourceOptions struct {
|
||||
ResourceType *azure.ResourceType
|
||||
ResourceTypeDisplayName string
|
||||
AllowNewResource bool
|
||||
SelectorOptions *PromptSelectOptions
|
||||
}
|
||||
|
||||
type PromptResourceGroupOptions struct {
|
||||
AllowNewResource bool
|
||||
SelectorOptions *PromptSelectOptions
|
||||
}
|
||||
|
||||
type PromptSelectOptions struct {
|
||||
Message string
|
||||
HelpMessage string
|
||||
LoadingMessage string
|
||||
DisplayNumbers *bool
|
||||
DisplayCount int
|
||||
}
|
||||
|
||||
func PromptSubscription(ctx context.Context, selectorOptions *PromptSelectOptions) (*azure.Subscription, error) {
|
||||
if selectorOptions == nil {
|
||||
selectorOptions = &PromptSelectOptions{}
|
||||
}
|
||||
|
||||
mergo.Merge(selectorOptions, &PromptSelectOptions{
|
||||
Message: "Select subscription",
|
||||
LoadingMessage: "Loading subscriptions...",
|
||||
HelpMessage: "Choose an Azure subscription for your project.",
|
||||
DisplayNumbers: ux.Ptr(true),
|
||||
DisplayCount: 10,
|
||||
})
|
||||
|
||||
azdContext, err := ext.CurrentContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
principal, err := azdContext.Principal(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
credential, err := azdContext.Credential()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userConfig, err := azdContext.UserConfig(ctx)
|
||||
if err != nil {
|
||||
log.Println("User config not found")
|
||||
}
|
||||
|
||||
subscriptionService := azure.NewSubscriptionsService(credential, nil)
|
||||
|
||||
loadingSpinner := ux.NewSpinner(&ux.SpinnerConfig{
|
||||
Text: selectorOptions.LoadingMessage,
|
||||
ClearOnStop: true,
|
||||
})
|
||||
|
||||
var subscriptions []*armsubscriptions.Subscription
|
||||
|
||||
err = loadingSpinner.Run(ctx, func(ctx context.Context) error {
|
||||
subscriptionList, err := subscriptionService.ListSubscriptions(ctx, principal.TenantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
subscriptions = subscriptionList
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var defaultIndex *int
|
||||
var defaultSubscriptionId = ""
|
||||
if userConfig != nil {
|
||||
subscriptionId, has := userConfig.GetString("defaults.subscription")
|
||||
if has {
|
||||
defaultSubscriptionId = subscriptionId
|
||||
}
|
||||
}
|
||||
|
||||
for i, subscription := range subscriptions {
|
||||
if *subscription.SubscriptionID == defaultSubscriptionId {
|
||||
defaultIndex = &i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
choices := make([]string, len(subscriptions))
|
||||
for i, subscription := range subscriptions {
|
||||
choices[i] = fmt.Sprintf("%s (%s)", *subscription.DisplayName, *subscription.SubscriptionID)
|
||||
}
|
||||
|
||||
subscriptionSelector := ux.NewSelect(&ux.SelectConfig{
|
||||
Message: selectorOptions.Message,
|
||||
HelpMessage: selectorOptions.HelpMessage,
|
||||
DisplayCount: selectorOptions.DisplayCount,
|
||||
DisplayNumbers: selectorOptions.DisplayNumbers,
|
||||
Allowed: choices,
|
||||
DefaultIndex: defaultIndex,
|
||||
})
|
||||
|
||||
selectedIndex, err := subscriptionSelector.Ask()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if selectedIndex == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
selectedSubscription := subscriptions[*selectedIndex]
|
||||
|
||||
return &azure.Subscription{
|
||||
Id: *selectedSubscription.SubscriptionID,
|
||||
Name: *selectedSubscription.DisplayName,
|
||||
TenantId: *selectedSubscription.TenantID,
|
||||
UserAccessTenantId: principal.TenantId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func PromptLocation(
|
||||
ctx context.Context,
|
||||
subscription *azure.Subscription,
|
||||
selectorOptions *PromptSelectOptions,
|
||||
) (*azure.Location, error) {
|
||||
if selectorOptions == nil {
|
||||
selectorOptions = &PromptSelectOptions{}
|
||||
}
|
||||
|
||||
mergo.Merge(selectorOptions, &PromptSelectOptions{
|
||||
Message: "Select location",
|
||||
LoadingMessage: "Loading locations...",
|
||||
HelpMessage: "Choose an Azure location for your project.",
|
||||
DisplayNumbers: ux.Ptr(true),
|
||||
DisplayCount: 10,
|
||||
})
|
||||
|
||||
azdContext, err := ext.CurrentContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
credential, err := azdContext.Credential()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userConfig, err := azdContext.UserConfig(ctx)
|
||||
if errors.Is(err, ext.ErrUserConfigNotFound) {
|
||||
log.Println("User config not found")
|
||||
}
|
||||
|
||||
loadingSpinner := ux.NewSpinner(&ux.SpinnerConfig{
|
||||
Text: selectorOptions.LoadingMessage,
|
||||
ClearOnStop: true,
|
||||
})
|
||||
|
||||
var locations []azure.Location
|
||||
|
||||
err = loadingSpinner.Run(ctx, func(ctx context.Context) error {
|
||||
subscriptionService := azure.NewSubscriptionsService(credential, nil)
|
||||
locationList, err := subscriptionService.ListSubscriptionLocations(ctx, subscription.Id, subscription.TenantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
locations = locationList
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var defaultIndex *int
|
||||
var defaultLocation = "eastus2"
|
||||
if userConfig != nil {
|
||||
location, has := userConfig.GetString("defaults.location")
|
||||
if has {
|
||||
defaultLocation = location
|
||||
}
|
||||
}
|
||||
|
||||
for i, location := range locations {
|
||||
if location.Name == defaultLocation {
|
||||
defaultIndex = &i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
choices := make([]string, len(locations))
|
||||
for i, location := range locations {
|
||||
choices[i] = fmt.Sprintf("%s (%s)", location.RegionalDisplayName, location.Name)
|
||||
}
|
||||
|
||||
locationSelector := ux.NewSelect(&ux.SelectConfig{
|
||||
Message: selectorOptions.Message,
|
||||
HelpMessage: selectorOptions.HelpMessage,
|
||||
DisplayCount: selectorOptions.DisplayCount,
|
||||
DisplayNumbers: selectorOptions.DisplayNumbers,
|
||||
Allowed: choices,
|
||||
DefaultIndex: defaultIndex,
|
||||
})
|
||||
|
||||
selectedIndex, err := locationSelector.Ask()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if selectedIndex == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
selectedLocation := locations[*selectedIndex]
|
||||
|
||||
return &azure.Location{
|
||||
Name: selectedLocation.Name,
|
||||
DisplayName: selectedLocation.DisplayName,
|
||||
RegionalDisplayName: selectedLocation.RegionalDisplayName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func PromptResourceGroup(
|
||||
ctx context.Context,
|
||||
subscription *azure.Subscription,
|
||||
options *PromptResourceGroupOptions,
|
||||
) (*azure.ResourceGroup, error) {
|
||||
if options == nil {
|
||||
options = &PromptResourceGroupOptions{}
|
||||
}
|
||||
|
||||
if options.SelectorOptions == nil {
|
||||
options.SelectorOptions = &PromptSelectOptions{}
|
||||
}
|
||||
|
||||
mergo.Merge(options.SelectorOptions, &PromptSelectOptions{
|
||||
Message: "Select resource group",
|
||||
LoadingMessage: "Loading resource groups...",
|
||||
HelpMessage: "Choose an Azure resource group for your project.",
|
||||
DisplayNumbers: ux.Ptr(true),
|
||||
DisplayCount: 10,
|
||||
})
|
||||
|
||||
azdContext, err := ext.CurrentContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
credential, err := azdContext.Credential()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
loadingSpinner := ux.NewSpinner(&ux.SpinnerConfig{
|
||||
Text: options.SelectorOptions.LoadingMessage,
|
||||
})
|
||||
|
||||
var resourceGroups []*azure.Resource
|
||||
|
||||
err = loadingSpinner.Run(ctx, func(ctx context.Context) error {
|
||||
resourceService := azure.NewResourceService(credential, nil)
|
||||
resourceGroupList, err := resourceService.ListResourceGroup(ctx, subscription.Id, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resourceGroups = resourceGroupList
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
choices := make([]string, len(resourceGroups))
|
||||
for i, resourceGroup := range resourceGroups {
|
||||
choices[i] = resourceGroup.Name
|
||||
}
|
||||
|
||||
resourceGroupSelector := ux.NewSelect(&ux.SelectConfig{
|
||||
Message: options.SelectorOptions.Message,
|
||||
HelpMessage: options.SelectorOptions.HelpMessage,
|
||||
DisplayCount: options.SelectorOptions.DisplayCount,
|
||||
DisplayNumbers: options.SelectorOptions.DisplayNumbers,
|
||||
Allowed: choices,
|
||||
})
|
||||
|
||||
selectedIndex, err := resourceGroupSelector.Ask()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if selectedIndex == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
selectedResourceGroup := resourceGroups[*selectedIndex]
|
||||
|
||||
return &azure.ResourceGroup{
|
||||
Id: selectedResourceGroup.Id,
|
||||
Name: selectedResourceGroup.Name,
|
||||
Location: selectedResourceGroup.Location,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func PromptSubscriptionResource(
|
||||
ctx context.Context,
|
||||
subscription *azure.Subscription,
|
||||
options PromptResourceOptions,
|
||||
) (*azure.Resource, error) {
|
||||
if options.SelectorOptions == nil {
|
||||
resourceName := options.ResourceTypeDisplayName
|
||||
|
||||
if resourceName == "" && options.ResourceType != nil {
|
||||
resourceName = string(*options.ResourceType)
|
||||
}
|
||||
|
||||
if resourceName == "" {
|
||||
resourceName = "resource"
|
||||
}
|
||||
|
||||
options.SelectorOptions = &PromptSelectOptions{
|
||||
Message: fmt.Sprintf("Select %s", resourceName),
|
||||
LoadingMessage: fmt.Sprintf("Loading %s resources...", resourceName),
|
||||
HelpMessage: fmt.Sprintf("Choose an Azure %s for your project.", resourceName),
|
||||
DisplayNumbers: ux.Ptr(true),
|
||||
DisplayCount: 10,
|
||||
}
|
||||
}
|
||||
|
||||
azdContext, err := ext.CurrentContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
credential, err := azdContext.Credential()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resourceListOptions *armresources.ClientListOptions
|
||||
if options.ResourceType != nil {
|
||||
resourceListOptions = &armresources.ClientListOptions{
|
||||
Filter: to.Ptr(fmt.Sprintf("resourceType eq '%s'", string(*options.ResourceType))),
|
||||
}
|
||||
}
|
||||
|
||||
loadingSpinner := ux.NewSpinner(&ux.SpinnerConfig{
|
||||
Text: options.SelectorOptions.LoadingMessage,
|
||||
})
|
||||
|
||||
var resources []*azure.Resource
|
||||
|
||||
err = loadingSpinner.Run(ctx, func(ctx context.Context) error {
|
||||
resourceService := azure.NewResourceService(credential, nil)
|
||||
resourceList, err := resourceService.ListSubscriptionResources(ctx, subscription.Id, resourceListOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resources = resourceList
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(resources) == 0 {
|
||||
return nil, fmt.Errorf("no resources found with type '%v'", options.ResourceType)
|
||||
}
|
||||
|
||||
choices := make([]string, len(resources))
|
||||
for i, resource := range resources {
|
||||
parsedResource, err := arm.ParseResourceID(resource.Id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing resource id: %w", err)
|
||||
}
|
||||
|
||||
choices[i] = fmt.Sprintf("%s (%s)", parsedResource.Name, parsedResource.ResourceGroupName)
|
||||
}
|
||||
|
||||
resourceSelector := ux.NewSelect(&ux.SelectConfig{
|
||||
Message: options.SelectorOptions.Message,
|
||||
HelpMessage: options.SelectorOptions.HelpMessage,
|
||||
DisplayCount: options.SelectorOptions.DisplayCount,
|
||||
DisplayNumbers: options.SelectorOptions.DisplayNumbers,
|
||||
Allowed: choices,
|
||||
})
|
||||
|
||||
selectedIndex, err := resourceSelector.Ask()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if selectedIndex == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return resources[*selectedIndex], nil
|
||||
}
|
||||
|
||||
func PromptResourceGroupResource(
|
||||
ctx context.Context,
|
||||
resourceGroup *azure.ResourceGroup,
|
||||
options PromptResourceOptions,
|
||||
) (*azure.Resource, error) {
|
||||
if options.SelectorOptions == nil {
|
||||
resourceName := options.ResourceTypeDisplayName
|
||||
|
||||
if resourceName == "" && options.ResourceType != nil {
|
||||
resourceName = string(*options.ResourceType)
|
||||
}
|
||||
|
||||
if resourceName == "" {
|
||||
resourceName = "resource"
|
||||
}
|
||||
|
||||
options.SelectorOptions = &PromptSelectOptions{
|
||||
Message: fmt.Sprintf("Select %s", resourceName),
|
||||
LoadingMessage: fmt.Sprintf("Loading %s resources...", resourceName),
|
||||
HelpMessage: fmt.Sprintf("Choose an Azure %s for your project.", resourceName),
|
||||
DisplayNumbers: ux.Ptr(true),
|
||||
DisplayCount: 10,
|
||||
}
|
||||
}
|
||||
|
||||
azdContext, err := ext.CurrentContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
credential, err := azdContext.Credential()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parsedResourceGroup, err := arm.ParseResourceID(resourceGroup.Id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing resource group id: %w", err)
|
||||
}
|
||||
|
||||
var resourceListOptions *azure.ListResourceGroupResourcesOptions
|
||||
if options.ResourceType != nil {
|
||||
resourceListOptions = &azure.ListResourceGroupResourcesOptions{
|
||||
Filter: to.Ptr(fmt.Sprintf("resourceType eq '%s'", *options.ResourceType)),
|
||||
}
|
||||
}
|
||||
|
||||
loadingSpinner := ux.NewSpinner(&ux.SpinnerConfig{
|
||||
Text: options.SelectorOptions.LoadingMessage,
|
||||
})
|
||||
|
||||
var resources []*azure.Resource
|
||||
|
||||
err = loadingSpinner.Run(ctx, func(ctx context.Context) error {
|
||||
resourceService := azure.NewResourceService(credential, nil)
|
||||
resourceList, err := resourceService.ListResourceGroupResources(
|
||||
ctx,
|
||||
parsedResourceGroup.SubscriptionID,
|
||||
resourceGroup.Name,
|
||||
resourceListOptions,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resources = resourceList
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(resources) == 0 {
|
||||
return nil, fmt.Errorf("no resources found with type '%v'", options.ResourceType)
|
||||
}
|
||||
|
||||
choices := make([]string, len(resources))
|
||||
for i, resource := range resources {
|
||||
choices[i] = resource.Name
|
||||
}
|
||||
|
||||
resourceSelector := ux.NewSelect(&ux.SelectConfig{
|
||||
Message: options.SelectorOptions.Message,
|
||||
HelpMessage: options.SelectorOptions.HelpMessage,
|
||||
DisplayCount: options.SelectorOptions.DisplayCount,
|
||||
DisplayNumbers: options.SelectorOptions.DisplayNumbers,
|
||||
Allowed: choices,
|
||||
})
|
||||
|
||||
selectedIndex, err := resourceSelector.Ask()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if selectedIndex == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return resources[*selectedIndex], nil
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
module github.com/azure/azure-dev/cli/sdk/azdcore
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azfile v1.3.1
|
||||
github.com/blang/semver/v4 v4.0.0
|
||||
github.com/drone/envsubst v1.0.3
|
||||
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203
|
||||
github.com/fatih/color v1.17.0
|
||||
github.com/golobby/container/v3 v3.3.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/microsoft/ApplicationInsights-Go v0.4.4
|
||||
github.com/microsoft/go-deviceid v1.0.0
|
||||
github.com/psanford/memfs v0.0.0-20240922203233-02fb08c0f8db
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.9.0
|
||||
go.opentelemetry.io/otel v1.30.0
|
||||
go.opentelemetry.io/otel/sdk v1.30.0
|
||||
go.opentelemetry.io/otel/trace v1.30.0
|
||||
go.uber.org/atomic v1.11.0
|
||||
golang.org/x/sys v0.26.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.30.0 // indirect
|
||||
golang.org/x/crypto v0.28.0 // indirect
|
||||
golang.org/x/net v0.30.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
)
|
|
@ -0,0 +1,143 @@
|
|||
code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c/go.mod h1:QD9Lzhd/ux6eNQVUDVRJX/RKTigpewimNYBi7ivZKY8=
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 h1:nyQWyZvwGTvunIMxi1Y9uXkcyr+I7TeNrr/foo4Kpk8=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0 h1:+m0M/LFxN43KvULkDNfdXOgrjtg6UYJPFBJyuEcRCAw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0/go.mod h1:PwOyop78lveYMRs6oCxjiVyBdyCgIYH6XHIVZO9/SFQ=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0/go.mod h1:LRr2FzBTQlONPPa5HREE5+RjSCTXl7BwOvYOaWTqCaI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0 h1:pPvTJ1dY0sA35JOeFq6TsY2xj6Z85Yo23Pj4wCCvu4o=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0/go.mod h1:mLfWfj8v3jfWKsL9G4eoBoXVcsqcIUTapmdKy7uGOp0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0 h1:wxQx2Bt4xzPIKvW59WQf1tJNx/ZZKPfN+EhPX3Z6CYY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0/go.mod h1:TpiwjwnW/khS0LKs4vW5UmmT9OWcxaveS8U7+tlknzo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.1 h1:cf+OIKbkmMHBaC3u78AXomweqM0oxQSgBXRZf3WH4yM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.1/go.mod h1:ap1dmS6vQKJxSMNiGJcq4QuUQkOynyD93gLw6MDF7ek=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azfile v1.3.1 h1:a1U6j4GPI18JQCqgz7/DcqXA1vzvGBugm14AXZfU0gs=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azfile v1.3.1/go.mod h1:tZyRNcHi2/yo+ugYHTUuOrHiboKilaizLnRL5aZTe6A=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g=
|
||||
github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g=
|
||||
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg=
|
||||
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg=
|
||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golobby/container/v3 v3.3.2 h1:7u+RgNnsdVlhGoS8gY4EXAG601vpMMzLZlYqSp77Quw=
|
||||
github.com/golobby/container/v3 v3.3.2/go.mod h1:RDdKpnKpV1Of11PFBe7Dxc2C1k2KaLE4FD47FflAmj0=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs=
|
||||
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/microsoft/ApplicationInsights-Go v0.4.4 h1:G4+H9WNs6ygSCe6sUyxRc2U81TI5Es90b2t/MwX5KqY=
|
||||
github.com/microsoft/ApplicationInsights-Go v0.4.4/go.mod h1:fKRUseBqkw6bDiXTs3ESTiU/4YTIHsQS4W3fP2ieF4U=
|
||||
github.com/microsoft/go-deviceid v1.0.0 h1:i5AQ654Xk9kfvwJeKQm3w2+eT1+ImBDVEpAR0AjpP40=
|
||||
github.com/microsoft/go-deviceid v1.0.0/go.mod h1:KY13FeVdHkzD8gy+6T8+kVmD/7RMpTaWW75K+T4uZWg=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/psanford/memfs v0.0.0-20240922203233-02fb08c0f8db h1:uwKfJcTGnZ4ziFqr0bjzNc/P1fydrAg450b9Dz8Fj8M=
|
||||
github.com/psanford/memfs v0.0.0-20240922203233-02fb08c0f8db/go.mod h1:tcaRap0jS3eifrEEllL6ZMd9dg8IlDpi2S1oARrQ+NI=
|
||||
github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4=
|
||||
github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tedsuo/ifrit v0.0.0-20180802180643-bea94bb476cc/go.mod h1:eyZnKCc955uh98WQvzOm0dgAeLnf2O0Rz0LPoC5ze+0=
|
||||
go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts=
|
||||
go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc=
|
||||
go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w=
|
||||
go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ=
|
||||
go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE=
|
||||
go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg=
|
||||
go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc=
|
||||
go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@ -0,0 +1,70 @@
|
|||
package installer
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type InstallType string
|
||||
|
||||
const InstallTypeUnknown InstallType = ""
|
||||
const InstallTypePs InstallType = "install-azd.ps1"
|
||||
const InstallTypeSh InstallType = "install-azd.sh"
|
||||
const InstallTypeBrew InstallType = "brew"
|
||||
const InstallTypeChoco InstallType = "choco"
|
||||
const InstallTypeWinget InstallType = "winget"
|
||||
const InstallTypeDeb InstallType = "deb"
|
||||
const InstallTypeRpm InstallType = "rpm"
|
||||
|
||||
// InstalledBy Returns the type of installer that installed the CLI, based on
|
||||
// the contents of the `.installed-by.txt` file in the same directory as the
|
||||
// executing instance of azd.
|
||||
func InstalledBy() InstallType {
|
||||
raw := RawInstalledBy()
|
||||
|
||||
switch raw {
|
||||
case string(InstallTypePs):
|
||||
return InstallTypePs
|
||||
case string(InstallTypeSh):
|
||||
return InstallTypeSh
|
||||
case string(InstallTypeBrew):
|
||||
return InstallTypeBrew
|
||||
case string(InstallTypeChoco):
|
||||
return InstallTypeChoco
|
||||
case string(InstallTypeWinget):
|
||||
return InstallTypeWinget
|
||||
case string(InstallTypeDeb):
|
||||
return InstallTypeDeb
|
||||
case string(InstallTypeRpm):
|
||||
return InstallTypeRpm
|
||||
default:
|
||||
return InstallTypeUnknown
|
||||
}
|
||||
}
|
||||
|
||||
// RawInstalledBy returns the raw value of the `.installed-by.txt` file in the
|
||||
// same directory as the executing instance of azd after removing leading and
|
||||
// trailing whitespace.
|
||||
func RawInstalledBy() string {
|
||||
exePath, err := os.Executable()
|
||||
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
resolvedPath, err := filepath.EvalSymlinks(exePath)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
exeDir := filepath.Dir(resolvedPath)
|
||||
installedByFile := filepath.Join(exeDir, ".installed-by.txt")
|
||||
|
||||
bytes, err := os.ReadFile(installedByFile)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(bytes))
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
//go:build !windows && !linux && !darwin
|
||||
|
||||
package osversion
|
||||
|
||||
import "errors"
|
||||
|
||||
func GetVersion() (string, error) {
|
||||
return "", errors.New("unsupported OS")
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package osversion
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c
|
||||
#cgo LDFLAGS: -framework Foundation
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Foundation/NSProcessInfo.h>
|
||||
|
||||
NSOperatingSystemVersion c_getVersion() {
|
||||
NSProcessInfo *pInfo = [NSProcessInfo processInfo];
|
||||
// check availability of the property operatingSystemVersion (10.10+) at runtime
|
||||
if ([pInfo respondsToSelector:@selector(operatingSystemVersion)])
|
||||
{
|
||||
return [pInfo operatingSystemVersion];
|
||||
}
|
||||
else
|
||||
{
|
||||
NSOperatingSystemVersion version;
|
||||
version.majorVersion = 10;
|
||||
version.minorVersion = 9;
|
||||
version.patchVersion = 0;
|
||||
return version;
|
||||
}
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func verToStr(ver C.NSOperatingSystemVersion) string {
|
||||
res := fmt.Sprintf("%d.%d.%d", int(ver.majorVersion), int(ver.minorVersion), int(ver.patchVersion))
|
||||
return res
|
||||
}
|
||||
|
||||
func doGetVersion() string {
|
||||
return verToStr(C.c_getVersion())
|
||||
}
|
||||
|
||||
func GetVersion() (string, error) {
|
||||
return doGetVersion(), nil
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package osversion
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func GetVersion() (string, error) {
|
||||
uname := unix.Utsname{}
|
||||
err := unix.Uname(&uname)
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unix.Uname returned error: %w", err)
|
||||
}
|
||||
|
||||
return string(uname.Release[:cStringLen(uname.Release[:])]), nil
|
||||
}
|
||||
|
||||
func cStringLen(s []byte) int {
|
||||
for i := range s {
|
||||
if s[i] == 0 {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return len(s)
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package osversion
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetVersion(t *testing.T) {
|
||||
ver, err := GetVersion()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, ver)
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package osversion
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func GetVersion() (string, error) {
|
||||
version := windows.RtlGetVersion()
|
||||
if version != nil {
|
||||
return fmt.Sprintf("%d.%d.%d", version.MajorVersion, version.MinorVersion, version.BuildNumber), nil
|
||||
}
|
||||
|
||||
return "", errors.New("windows.RtlGetVersion returned nil")
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package runcontext
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// AzdInCloudShellEnvVar is the environment variable that is set when running in Cloud Shell. It is set to a value recognized
|
||||
// by strconv.ParseBool.
|
||||
//
|
||||
// Use [IsRunningInCloudShell] to check if the current process is running in Cloud Shell.
|
||||
const AzdInCloudShellEnvVar = "AZD_IN_CLOUDSHELL"
|
||||
|
||||
func IsRunningInCloudShell() bool {
|
||||
if azdInCloudShell, has := os.LookupEnv(AzdInCloudShellEnvVar); has {
|
||||
if use, err := strconv.ParseBool(azdInCloudShell); err == nil && use {
|
||||
log.Printf("running in Cloud Shell")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
|
@ -0,0 +1,193 @@
|
|||
package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/internal/tracing/baggage"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
// SetBaggageInContext sets the given attributes as baggage.
|
||||
// Baggage attributes are set for the current running span, and for any child spans created.
|
||||
func SetBaggageInContext(ctx context.Context, attributes ...attribute.KeyValue) context.Context {
|
||||
SetAttributesInContext(ctx, attributes...)
|
||||
return baggage.ContextWithAttributes(ctx, attributes)
|
||||
}
|
||||
|
||||
// SetAttributesInContext sets the given attributes for the current running span.
|
||||
func SetAttributesInContext(ctx context.Context, attributes ...attribute.KeyValue) {
|
||||
runningSpan := trace.SpanFromContext(ctx)
|
||||
runningSpan.SetAttributes(attributes...)
|
||||
}
|
||||
|
||||
// Attributes that are global and set on all events
|
||||
var globalVal = valSynced{}
|
||||
|
||||
// Attributes that are only set on command-level usage events
|
||||
var usageVal = valSynced{}
|
||||
|
||||
// Atomic value with mutex for multiple writers
|
||||
type valSynced struct {
|
||||
val atomic.Value
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func init() {
|
||||
globalVal.val.Store(baggage.NewBaggage())
|
||||
usageVal.val.Store(baggage.NewBaggage())
|
||||
}
|
||||
|
||||
func set(v *valSynced, attributes []attribute.KeyValue) {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
|
||||
baggage := v.val.Load().(baggage.Baggage)
|
||||
newBaggage := baggage.Set(attributes...)
|
||||
|
||||
v.val.Store(newBaggage)
|
||||
}
|
||||
|
||||
func get(v *valSynced) []attribute.KeyValue {
|
||||
baggage := v.val.Load().(baggage.Baggage)
|
||||
return baggage.Attributes()
|
||||
}
|
||||
|
||||
func appendTo(v *valSynced, attr attribute.KeyValue) {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
|
||||
baggage := v.val.Load().(baggage.Baggage)
|
||||
val, ok := baggage.Lookup(attr.Key)
|
||||
if ok && val.Type() == attr.Value.Type() {
|
||||
switch attr.Value.Type() {
|
||||
case attribute.BOOLSLICE:
|
||||
attr = attr.Key.BoolSlice(append(val.AsBoolSlice(), attr.Value.AsBoolSlice()...))
|
||||
case attribute.INT64SLICE:
|
||||
attr = attr.Key.Int64Slice(append(val.AsInt64Slice(), attr.Value.AsInt64Slice()...))
|
||||
case attribute.FLOAT64SLICE:
|
||||
attr = attr.Key.Float64Slice(append(val.AsFloat64Slice(), attr.Value.AsFloat64Slice()...))
|
||||
case attribute.STRINGSLICE:
|
||||
attr = attr.Key.StringSlice(append(val.AsStringSlice(), attr.Value.AsStringSlice()...))
|
||||
}
|
||||
}
|
||||
|
||||
newBaggage := baggage.Set(attr)
|
||||
v.val.Store(newBaggage)
|
||||
}
|
||||
|
||||
func appendToUnique(v *valSynced, attr attribute.KeyValue) {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
|
||||
baggage := v.val.Load().(baggage.Baggage)
|
||||
val, ok := baggage.Lookup(attr.Key)
|
||||
if ok && val.Type() == attribute.STRINGSLICE {
|
||||
switch attr.Value.Type() {
|
||||
case attribute.STRING:
|
||||
attrValue := attr.Value.AsString()
|
||||
for _, v := range val.AsStringSlice() {
|
||||
if v == attrValue { // already exists
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
attr = attr.Key.StringSlice(append(val.AsStringSlice(), attrValue))
|
||||
case attribute.STRINGSLICE:
|
||||
attrValues := attr.Value.AsStringSlice()
|
||||
adds := make([]string, 0, len(attrValues))
|
||||
|
||||
for _, a := range attrValues {
|
||||
if !slices.Contains(val.AsStringSlice(), a) {
|
||||
adds = append(adds, a)
|
||||
}
|
||||
}
|
||||
|
||||
if len(adds) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
attr = attr.Key.StringSlice(append(val.AsStringSlice(), adds...))
|
||||
}
|
||||
} else if attr.Value.Type() == attribute.STRING {
|
||||
attr = attr.Key.StringSlice([]string{attr.Value.AsString()})
|
||||
}
|
||||
|
||||
newBaggage := baggage.Set(attr)
|
||||
v.val.Store(newBaggage)
|
||||
}
|
||||
|
||||
func increment(v *valSynced, attr attribute.KeyValue) {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
|
||||
baggage := v.val.Load().(baggage.Baggage)
|
||||
val, ok := baggage.Lookup(attr.Key)
|
||||
if ok && val.Type() == attr.Value.Type() {
|
||||
switch attr.Value.Type() {
|
||||
case attribute.INT64:
|
||||
attr = attr.Key.Int64(val.AsInt64() + attr.Value.AsInt64())
|
||||
case attribute.FLOAT64:
|
||||
attr = attr.Key.Float64(val.AsFloat64() + attr.Value.AsFloat64())
|
||||
case attribute.STRING:
|
||||
attr = attr.Key.String(val.AsString() + attr.Value.AsString())
|
||||
}
|
||||
}
|
||||
|
||||
newBaggage := baggage.Set(attr)
|
||||
v.val.Store(newBaggage)
|
||||
}
|
||||
|
||||
// Sets global attributes that are included with all telemetry events emitted.
|
||||
// If the attribute already exists, the value is replaced.
|
||||
func SetGlobalAttributes(attributes ...attribute.KeyValue) {
|
||||
set(&globalVal, attributes)
|
||||
}
|
||||
|
||||
// Returns all global attributes set.
|
||||
func GetGlobalAttributes() []attribute.KeyValue {
|
||||
return get(&globalVal)
|
||||
}
|
||||
|
||||
// Sets usage attributes that are included with usage events emitted.
|
||||
// If the attribute already exists, the value is replaced.
|
||||
func SetUsageAttributes(attributes ...attribute.KeyValue) {
|
||||
set(&usageVal, attributes)
|
||||
}
|
||||
|
||||
// Returns all usage attributes set.
|
||||
func GetUsageAttributes() []attribute.KeyValue {
|
||||
return get(&usageVal)
|
||||
}
|
||||
|
||||
// Sets or appends a value to a slice-type usage attribute that possibly exists.
|
||||
// The attribute is expected to be a slice-type value, and matches the existing type.
|
||||
// Otherwise, a strict replacement is performed.
|
||||
func AppendUsageAttribute(attr attribute.KeyValue) {
|
||||
appendTo(&usageVal, attr)
|
||||
}
|
||||
|
||||
// Sets or appends a value to a string slice-type usage attribute that possibly exists,
|
||||
// merging on unique elements.
|
||||
//
|
||||
// The attribute is expected to be a slice-type value, and matches the existing type.
|
||||
// For convenience, a string-type value is also treated as a string-slice with a single element.
|
||||
//
|
||||
// If the types do not match, a strict replacement is performed.
|
||||
func AppendUsageAttributeUnique(attr attribute.KeyValue) {
|
||||
appendToUnique(&usageVal, attr)
|
||||
}
|
||||
|
||||
// Sets or increments a possibly stored usage attribute.
|
||||
// The attribute is expected to be a numeric-type or string-type value, and matches the existing type
|
||||
// of a previously stored usage attribute.
|
||||
// Otherwise, a strict replacement is performed.
|
||||
func IncrementUsageAttribute(attr attribute.KeyValue) {
|
||||
increment(&usageVal, attr)
|
||||
}
|
||||
|
||||
// InteractTimeMs is the time spent waiting on user interaction in milliseconds.
|
||||
var InteractTimeMs = atomic.NewInt64(0)
|
|
@ -0,0 +1,200 @@
|
|||
package tracing
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/internal/tracing/baggage"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
func TestSetAndGetAttributes(t *testing.T) {
|
||||
val := &valSynced{}
|
||||
val.val.Store(baggage.NewBaggage())
|
||||
setAttributes := []attribute.KeyValue{
|
||||
attribute.String("str", "val1"),
|
||||
attribute.Bool("bool", true),
|
||||
attribute.Int64("int64", 10),
|
||||
attribute.Float64("float64", 10.0),
|
||||
|
||||
attribute.StringSlice("stringslice", []string{"v1", "v2"}),
|
||||
attribute.BoolSlice("boolslice", []bool{false, true}),
|
||||
attribute.Int64Slice("int64slice", []int64{1, 2}),
|
||||
attribute.Float64Slice("float64slice", []float64{1.0, 2.0}),
|
||||
}
|
||||
set(val, setAttributes)
|
||||
|
||||
actual := get(val)
|
||||
assert.ElementsMatch(t, actual, setAttributes)
|
||||
}
|
||||
|
||||
func TestAppendAttribute(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
existing []attribute.KeyValue
|
||||
set attribute.KeyValue
|
||||
expected []attribute.KeyValue
|
||||
}{
|
||||
{"Set", []attribute.KeyValue{}, attribute.String("k", "v"), []attribute.KeyValue{attribute.String("k", "v")}},
|
||||
{"SetSlice",
|
||||
[]attribute.KeyValue{},
|
||||
attribute.StringSlice("k", []string{"v"}),
|
||||
[]attribute.KeyValue{attribute.StringSlice("k", []string{"v"})}},
|
||||
|
||||
{"ReplaceWhenUnmatched",
|
||||
[]attribute.KeyValue{attribute.StringSlice("k", []string{"v1"})},
|
||||
attribute.String("k", "v"),
|
||||
[]attribute.KeyValue{attribute.String("k", "v")}},
|
||||
|
||||
{"AppendStringSlice",
|
||||
[]attribute.KeyValue{attribute.StringSlice("k", []string{"v1"})},
|
||||
attribute.StringSlice("k", []string{"v2"}),
|
||||
[]attribute.KeyValue{attribute.StringSlice("k", []string{"v1", "v2"})}},
|
||||
|
||||
{"AppendBoolSlice",
|
||||
[]attribute.KeyValue{attribute.BoolSlice("k", []bool{true})},
|
||||
attribute.BoolSlice("k", []bool{false}),
|
||||
[]attribute.KeyValue{attribute.BoolSlice("k", []bool{true, false})}},
|
||||
|
||||
{"AppendInt64Slice",
|
||||
[]attribute.KeyValue{attribute.Int64Slice("k", []int64{1})},
|
||||
attribute.Int64Slice("k", []int64{2}),
|
||||
[]attribute.KeyValue{attribute.Int64Slice("k", []int64{1, 2})}},
|
||||
|
||||
{"AppendFloat64Slice",
|
||||
[]attribute.KeyValue{attribute.Float64Slice("k", []float64{1.0})},
|
||||
attribute.Float64Slice("k", []float64{2.0}),
|
||||
[]attribute.KeyValue{attribute.Float64Slice("k", []float64{1.0, 2.0})}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
val := &valSynced{}
|
||||
val.val.Store(baggage.NewBaggage())
|
||||
|
||||
set(val, tt.existing)
|
||||
|
||||
appendTo(val, tt.set)
|
||||
|
||||
attributes := get(val)
|
||||
|
||||
assert.ElementsMatch(t, attributes, tt.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendAttributeUnique(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
existing []attribute.KeyValue
|
||||
set attribute.KeyValue
|
||||
expected []attribute.KeyValue
|
||||
}{
|
||||
{"SetString",
|
||||
[]attribute.KeyValue{},
|
||||
attribute.String("k", "v"),
|
||||
[]attribute.KeyValue{attribute.StringSlice("k", []string{"v"})}},
|
||||
{"SetSlice",
|
||||
[]attribute.KeyValue{},
|
||||
attribute.StringSlice("k", []string{"v"}),
|
||||
[]attribute.KeyValue{attribute.StringSlice("k", []string{"v"})}},
|
||||
|
||||
{"ReplaceStringWhenUnmatched",
|
||||
[]attribute.KeyValue{attribute.BoolSlice("k", []bool{true})},
|
||||
attribute.String("k", "v"),
|
||||
[]attribute.KeyValue{attribute.StringSlice("k", []string{"v"})}},
|
||||
|
||||
{"ReplaceStringSliceWhenUnmatched",
|
||||
[]attribute.KeyValue{attribute.BoolSlice("k", []bool{true})},
|
||||
attribute.StringSlice("k", []string{"v"}),
|
||||
[]attribute.KeyValue{attribute.StringSlice("k", []string{"v"})}},
|
||||
|
||||
{"AppendStringSlice",
|
||||
[]attribute.KeyValue{attribute.StringSlice("k", []string{"v1"})},
|
||||
attribute.StringSlice("k", []string{"v2"}),
|
||||
[]attribute.KeyValue{attribute.StringSlice("k", []string{"v1", "v2"})}},
|
||||
|
||||
{"MergeStringSlice",
|
||||
[]attribute.KeyValue{attribute.StringSlice("k", []string{"v1", "v2"})},
|
||||
attribute.StringSlice("k", []string{"v2", "v3"}),
|
||||
[]attribute.KeyValue{attribute.StringSlice("k", []string{"v1", "v2", "v3"})}},
|
||||
|
||||
{"ExistingStringSlice",
|
||||
[]attribute.KeyValue{attribute.StringSlice("k", []string{"v1", "v2"})},
|
||||
attribute.StringSlice("k", []string{"v2"}),
|
||||
[]attribute.KeyValue{attribute.StringSlice("k", []string{"v1", "v2"})}},
|
||||
|
||||
{"AppendString",
|
||||
[]attribute.KeyValue{attribute.StringSlice("k", []string{"v1"})},
|
||||
attribute.String("k", "v2"),
|
||||
[]attribute.KeyValue{attribute.StringSlice("k", []string{"v1", "v2"})}},
|
||||
|
||||
{"MergeString",
|
||||
[]attribute.KeyValue{attribute.StringSlice("k", []string{"v1", "v2"})},
|
||||
attribute.String("k", "v3"),
|
||||
[]attribute.KeyValue{attribute.StringSlice("k", []string{"v1", "v2", "v3"})}},
|
||||
|
||||
{"ExistingString",
|
||||
[]attribute.KeyValue{attribute.StringSlice("k", []string{"v1", "v2"})},
|
||||
attribute.String("k", "v2"),
|
||||
[]attribute.KeyValue{attribute.StringSlice("k", []string{"v1", "v2"})}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
val := &valSynced{}
|
||||
val.val.Store(baggage.NewBaggage())
|
||||
|
||||
set(val, tt.existing)
|
||||
|
||||
appendToUnique(val, tt.set)
|
||||
|
||||
attributes := get(val)
|
||||
|
||||
assert.ElementsMatch(t, attributes, tt.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncrementAttribute(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
existing []attribute.KeyValue
|
||||
set attribute.KeyValue
|
||||
expected []attribute.KeyValue
|
||||
}{
|
||||
{"SetUnknown", []attribute.KeyValue{}, attribute.String("k", "v"), []attribute.KeyValue{attribute.String("k", "v")}},
|
||||
{"Set",
|
||||
[]attribute.KeyValue{},
|
||||
attribute.Float64("k", 5.0),
|
||||
[]attribute.KeyValue{attribute.Float64("k", 5.0)}},
|
||||
{"ReplaceWhenUnmatched",
|
||||
[]attribute.KeyValue{attribute.StringSlice("k", []string{"v1"})},
|
||||
attribute.Float64("k", 5.0),
|
||||
[]attribute.KeyValue{attribute.Float64("k", 5.0)}},
|
||||
{"IncrementFloat64",
|
||||
[]attribute.KeyValue{attribute.Float64("k", 5.0)},
|
||||
attribute.Float64("k", 5.0),
|
||||
[]attribute.KeyValue{attribute.Float64("k", 10.0)}},
|
||||
{"IncrementInt64",
|
||||
[]attribute.KeyValue{attribute.Int64("k", 5)},
|
||||
attribute.Int64("k", 5),
|
||||
[]attribute.KeyValue{attribute.Int64("k", 10)}},
|
||||
{"ConcatenateString",
|
||||
[]attribute.KeyValue{attribute.String("k", "v")},
|
||||
attribute.String("k", "v"),
|
||||
[]attribute.KeyValue{attribute.String("k", "vv")}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
val := &valSynced{}
|
||||
val.val.Store(baggage.NewBaggage())
|
||||
|
||||
set(val, tt.existing)
|
||||
|
||||
increment(val, tt.set)
|
||||
|
||||
attributes := get(val)
|
||||
|
||||
assert.ElementsMatch(t, attributes, tt.expected)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package baggage
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"slices"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
// An immutable object safe for concurrent use.
|
||||
type Baggage struct {
|
||||
// Use a map to avoid duplicates as OpenTelemetry does not allow for duplicate attributes
|
||||
|
||||
m map[attribute.Key]attribute.Value
|
||||
}
|
||||
|
||||
// NewBaggage creates a properly-constructed Baggage.
|
||||
func NewBaggage() Baggage {
|
||||
return Baggage{m: map[attribute.Key]attribute.Value{}}
|
||||
}
|
||||
|
||||
func (mb Baggage) copy() Baggage {
|
||||
copied := Baggage{m: make(map[attribute.Key]attribute.Value, len(mb.m))}
|
||||
for k, v := range mb.m {
|
||||
copied.m[k] = v
|
||||
}
|
||||
|
||||
return copied
|
||||
}
|
||||
|
||||
// Set sets the provided key value pairs, returning a new copy of Baggage.
|
||||
// For any existing keys, the value is overridden.
|
||||
func (mb Baggage) Set(keyValue ...attribute.KeyValue) Baggage {
|
||||
if len(keyValue) == 0 {
|
||||
return mb
|
||||
}
|
||||
|
||||
copied := mb.copy()
|
||||
for _, kv := range keyValue {
|
||||
copied.m[kv.Key] = kv.Value
|
||||
}
|
||||
|
||||
return copied
|
||||
}
|
||||
|
||||
// Delete removes the provided key, returning a new copy of Baggage.
|
||||
// The new Baggage copy is returned that should be used.
|
||||
func (mb Baggage) Delete(key attribute.Key) Baggage {
|
||||
copied := Baggage{m: make(map[attribute.Key]attribute.Value, len(mb.m))}
|
||||
for k, v := range mb.m {
|
||||
if k != key {
|
||||
copied.m[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return copied
|
||||
}
|
||||
|
||||
// Lookup returns the value of the given key.
|
||||
// If the key does not exist, the boolean value returned is false.
|
||||
func (mb Baggage) Lookup(key attribute.Key) (val attribute.Value, ok bool) {
|
||||
val, ok = mb.m[key]
|
||||
return
|
||||
}
|
||||
|
||||
// Get returns the value of the given key.
|
||||
// If the key does not exist, the default value is returned.
|
||||
func (mb Baggage) Get(key attribute.Key) attribute.Value {
|
||||
return mb.m[key]
|
||||
}
|
||||
|
||||
// Keys returns a copy of the keys contained.
|
||||
func (mb Baggage) Keys() []attribute.Key {
|
||||
return slices.Collect(maps.Keys(mb.m))
|
||||
}
|
||||
|
||||
// Attributes returns a copy of the key-value attributes contained.
|
||||
func (mb Baggage) Attributes() []attribute.KeyValue {
|
||||
res := make([]attribute.KeyValue, mb.Len())
|
||||
i := 0
|
||||
for k, v := range mb.m {
|
||||
res[i] = attribute.KeyValue{Key: k, Value: v}
|
||||
i++
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// Len returns the number of attributes contained.
|
||||
func (mb Baggage) Len() int {
|
||||
return len(mb.m)
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package baggage
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
func TestBaggage(t *testing.T) {
|
||||
baggage := NewBaggage()
|
||||
attr := attribute.String("key1", "val1")
|
||||
newBaggage := baggage.Set(attr)
|
||||
assert.Equal(t, newBaggage.Get("key1").AsString(), "val1")
|
||||
|
||||
attributes := []attribute.KeyValue{attribute.String("key2", "val2"), attribute.Int("key3", 3)}
|
||||
newBaggage = newBaggage.Set(attributes...)
|
||||
assert.Equal(t, newBaggage.Len(), 3)
|
||||
assert.ElementsMatch(t, newBaggage.Keys(), []attribute.Key{"key1", "key2", "key3"})
|
||||
assert.ElementsMatch(t, newBaggage.Attributes(), append(attributes, attr))
|
||||
assert.Equal(t, newBaggage.Get("key1").AsString(), "val1")
|
||||
assert.Equal(t, newBaggage.Get("key2").AsString(), "val2")
|
||||
assert.Equal(t, newBaggage.Get("key3").AsInt64(), int64(3))
|
||||
|
||||
newBaggage = newBaggage.Delete("key2")
|
||||
assertNotFound(t, newBaggage, "key2")
|
||||
assert.Equal(t, newBaggage.Len(), 2)
|
||||
assert.ElementsMatch(t, newBaggage.Keys(), []attribute.Key{"key1", "key3"})
|
||||
|
||||
newBaggage = newBaggage.Delete("key3")
|
||||
assertNotFound(t, newBaggage, "key3")
|
||||
assert.Equal(t, newBaggage.Len(), 1)
|
||||
assert.ElementsMatch(t, newBaggage.Keys(), []attribute.Key{"key1"})
|
||||
|
||||
newBaggage = newBaggage.Delete("key1")
|
||||
assertNotFound(t, newBaggage, "key1")
|
||||
assert.Equal(t, newBaggage.Len(), 0)
|
||||
assert.ElementsMatch(t, newBaggage.Keys(), []attribute.Key{})
|
||||
}
|
||||
|
||||
func TestBaggageMutateCreatesCopy(t *testing.T) {
|
||||
empty := NewBaggage()
|
||||
withKey := empty.Set(attribute.String("key1", "val1"))
|
||||
|
||||
assertEmptyUnchanged := func() {
|
||||
assertNotFound(t, empty, "key1")
|
||||
assert.Equal(t, 1, withKey.Len())
|
||||
}
|
||||
|
||||
assertWithKeyUnchanged := func() {
|
||||
assert.Equal(t, "val1", withKey.Get("key1").AsString())
|
||||
assert.Equal(t, 1, withKey.Len())
|
||||
}
|
||||
assertWithKeyUnchanged()
|
||||
assertEmptyUnchanged()
|
||||
|
||||
withoutKey := withKey.Delete("key1")
|
||||
assertWithKeyUnchanged()
|
||||
assertEmptyUnchanged()
|
||||
assertNotFound(t, withoutKey, "key1")
|
||||
|
||||
withKeyModified := withKey.Set(attribute.String("key1", "updated1"), attribute.String("key2", "val2"))
|
||||
assertWithKeyUnchanged()
|
||||
assertEmptyUnchanged()
|
||||
assert.Equal(t, "updated1", withKeyModified.Get("key1").AsString())
|
||||
assert.Equal(t, "val2", withKeyModified.Get("key2").AsString())
|
||||
assert.Equal(t, 2, withKeyModified.Len())
|
||||
}
|
||||
|
||||
func TestBaggageWhenEmpty(t *testing.T) {
|
||||
baggage := NewBaggage()
|
||||
newBaggage := baggage.Delete("notFound")
|
||||
assert.Equal(t, newBaggage.Len(), 0)
|
||||
|
||||
newBaggage = baggage.Set()
|
||||
assert.Equal(t, newBaggage.Len(), 0)
|
||||
}
|
||||
|
||||
func assertNotFound(t *testing.T, baggage Baggage, key attribute.Key) {
|
||||
val, ok := baggage.Lookup(key)
|
||||
assert.False(t, ok)
|
||||
assert.Equal(t, attribute.Value{}, val)
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
// Package baggage provides an implementation of storing trace-level context data, i.e. baggage.
|
||||
// Unlike OpenTelemetry's implementation of baggage, this baggage is only propagated to child spans of the current local
|
||||
// process.
|
||||
// Information is not propagated with any external calls.
|
||||
package baggage
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
const baggageKey contextKey = "baggageState"
|
||||
|
||||
// ContextWithAttributes returns a copy of ctx with attributes set.
|
||||
func ContextWithAttributes(ctx context.Context, attributes []attribute.KeyValue) context.Context {
|
||||
bg := BaggageFromContext(ctx)
|
||||
bg = bg.Set(attributes...)
|
||||
return ContextWithBaggage(ctx, bg)
|
||||
}
|
||||
|
||||
// ContextWithBaggage returns a copy of ctx with baggage.
|
||||
func ContextWithBaggage(ctx context.Context, tv Baggage) context.Context {
|
||||
return context.WithValue(ctx, baggageKey, tv.m)
|
||||
}
|
||||
|
||||
// ContextWithoutBaggage returns a copy of ctx with no baggage.
|
||||
func ContextWithoutBaggage(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, baggageKey, NewBaggage())
|
||||
}
|
||||
|
||||
// FromContext returns the baggage contained in ctx.
|
||||
func BaggageFromContext(ctx context.Context) Baggage {
|
||||
state, ok := ctx.Value(baggageKey).(map[attribute.Key]attribute.Value)
|
||||
if !ok || state == nil {
|
||||
return NewBaggage()
|
||||
}
|
||||
|
||||
return Baggage{m: state}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package baggage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
func TestContext(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
assert.Equal(t, NewBaggage(), BaggageFromContext(ctx))
|
||||
|
||||
b := NewBaggage()
|
||||
b.Set(attribute.String("key1", "val1"))
|
||||
ctx = ContextWithBaggage(ctx, b)
|
||||
assert.Equal(t, b, BaggageFromContext(ctx))
|
||||
|
||||
ctx = ContextWithoutBaggage(ctx)
|
||||
assert.Equal(t, NewBaggage(), BaggageFromContext(ctx))
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
// Package events provides definitions and functions related to the definition of telemetry events.
|
||||
package events
|
||||
|
||||
// Command event names follow the convention cmd.<command invocation path with spaces replaced by .>.
|
||||
//
|
||||
// Examples:
|
||||
// - cmd.auth.login
|
||||
// - cmd.init
|
||||
// - cmd.up
|
||||
const CommandEventPrefix = "cmd."
|
||||
|
||||
// Prefix for vsrpc events.
|
||||
const VsRpcEventPrefix = "vsrpc."
|
||||
|
||||
// PackBuildEvent is the name of the event which tracks the overall pack build operation.
|
||||
const PackBuildEvent = "tools.pack.build"
|
|
@ -0,0 +1,21 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package events
|
||||
|
||||
import "strings"
|
||||
|
||||
// GetCommandEventName returns the event name for CLI commands.
|
||||
func GetCommandEventName(cmdPath string) string {
|
||||
return CommandEventPrefix + formatCommandPath(cmdPath)
|
||||
}
|
||||
|
||||
// formatCommandPath reformats the command path suitable for telemetry emission.
|
||||
//
|
||||
// It removes "azd" from command path and replaces spaces with dot.
|
||||
//
|
||||
// Example: "azd env list" -> "env.list"
|
||||
func formatCommandPath(cmdPath string) string {
|
||||
cmdPath = strings.TrimPrefix(cmdPath, "azd ")
|
||||
return strings.ReplaceAll(cmdPath, " ", ".")
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package events
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetCommandEventName(t *testing.T) {
|
||||
type args struct {
|
||||
cmdPath string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{"Single", args{"azd provision"}, "cmd.provision"},
|
||||
{"Multiple", args{"azd env list"}, "cmd.env.list"},
|
||||
{"SpecialChar", args{"azd env get-values"}, "cmd.env.get-values"},
|
||||
|
||||
// These cases should not happen in the actual application.
|
||||
// However, we should be lenient in formatting these and not error.
|
||||
{"LenientSingle", args{"provision"}, "cmd.provision"},
|
||||
{"LenientMultiple", args{"env list"}, "cmd.env.list"},
|
||||
{"LenientSpecialChar", args{"env get-values"}, "cmd.env.get-values"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
eventName := GetCommandEventName(tt.args.cmdPath)
|
||||
assert.Equal(t, eventName, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package fields
|
||||
|
||||
type Domain struct {
|
||||
// The domain name.
|
||||
Name string
|
||||
// The name of the service that is responsible for the domain name.
|
||||
Service string
|
||||
}
|
||||
|
||||
// Well-known domains. Domains can also be subdomains, thus should be evaluated as such.
|
||||
//
|
||||
// Taken from https://learn.microsoft.com/azure/security/fundamentals/azure-domains.
|
||||
var Domains = []Domain{
|
||||
// Order here matters, as it likely determines evaluation precedence due to short-circuiting.
|
||||
{"dev.azure.com", "azdo"},
|
||||
{"management.azure.com", "arm"},
|
||||
{"management.core.windows.net", "arm"},
|
||||
{"graph.microsoft.com", "graph"},
|
||||
{"graph.windows.net", "graph"},
|
||||
{"azmk8s.io", "aks"},
|
||||
{"azure-api.net", "apim"},
|
||||
{"azure-mobile.net", "mobile"},
|
||||
{"azurecontainerapps.io", "aca"},
|
||||
{"azurecr.io", "acr"},
|
||||
{"azureedge.net", "edge"},
|
||||
{"azurefd.net", "frontdoor"},
|
||||
{"scm.azurewebsites.net", "kudu"},
|
||||
{"azurewebsites.net", "websites"},
|
||||
{"blob.core.windows.net", "blob"},
|
||||
{"cloudapp.azure.com", "vm"},
|
||||
{"cloudapp.net", "vm"},
|
||||
{"cosmos.azure.com", "cosmos"},
|
||||
{"database.windows.net", "sql"},
|
||||
{"documents.azure.com", "cosmos"},
|
||||
{"file.core.windows.net", "files"},
|
||||
{"origin.mediaservices.windows.net", "media"},
|
||||
{"queue.core.windows.net", "queue"},
|
||||
{"servicebus.windows.net", "servicebus"},
|
||||
{"table.core.windows.net", "table"},
|
||||
{"trafficmanager.net", "trafficmanager"},
|
||||
{"vault.azure.net", "keyvault"},
|
||||
{"visualstudio.com", "vs"},
|
||||
{"vo.msecnd.net", "cdn"},
|
||||
}
|
|
@ -0,0 +1,273 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
// Package fields provides definitions and functions related to the definition of telemetry fields.
|
||||
package fields
|
||||
|
||||
import (
|
||||
"github.com/microsoft/ApplicationInsights-Go/appinsights/contracts"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
|
||||
)
|
||||
|
||||
// Application-level fields. Guaranteed to be set and available for all events.
|
||||
const (
|
||||
// Application name. Value is always "azd".
|
||||
ServiceNameKey = semconv.ServiceNameKey // service.name
|
||||
// Application version.
|
||||
ServiceVersionKey = semconv.ServiceVersionKey // service.version
|
||||
|
||||
// The operating system type.
|
||||
OSTypeKey = semconv.OSTypeKey // os.type
|
||||
|
||||
// The operating system version.
|
||||
//
|
||||
// Examples:
|
||||
// - On Windows systems: The Windows version 10.x.x
|
||||
// - On Unix-based systems: The release portion of uname. https://en.wikipedia.org/wiki/Uname#Examples
|
||||
// - On MacOS: The MacOS version. For example: 12.5.1 for macOS Monterey
|
||||
OSVersionKey = semconv.OSVersionKey // os.version
|
||||
|
||||
// The CPU architecture the host system is running on.
|
||||
HostArchKey = semconv.HostArchKey // host.arch
|
||||
|
||||
// The version of the runtime of this process, as returned by the runtime without
|
||||
// modification.
|
||||
ProcessRuntimeVersionKey = semconv.ProcessRuntimeVersionKey // process.runtime.version
|
||||
|
||||
// A unique ID associated to the machine the application is installed on.
|
||||
//
|
||||
// This shares implementation with VSCode's machineId and can match exactly on a given device, although there are no
|
||||
// guarantees.
|
||||
MachineIdKey = attribute.Key("machine.id")
|
||||
|
||||
// The unique DevDeviceId associated with the device.
|
||||
DevDeviceIdKey = attribute.Key("machine.devdeviceid")
|
||||
|
||||
// An enumeration of possible environments that the application is running on.
|
||||
//
|
||||
// Example: Desktop, Azure Pipelines, Visual Studio.
|
||||
//
|
||||
// See EnvDesktop for complete set of values.
|
||||
ExecutionEnvironmentKey = attribute.Key("execution.environment")
|
||||
|
||||
// Installer used to install the application. Set in .installed-by.txt file
|
||||
// located in the same folder as the executable.
|
||||
//
|
||||
// Example: "msi", "brew", "choco", "rpm", "deb"
|
||||
InstalledByKey = attribute.Key("service.installer")
|
||||
)
|
||||
|
||||
// Fields related to the experimentation platform
|
||||
const (
|
||||
// The assignment context as returned by the experimentation platform.
|
||||
ExpAssignmentContextKey = attribute.Key("exp.assignmentContext")
|
||||
)
|
||||
|
||||
// Context level fields. Availability depends on the command running.
|
||||
const (
|
||||
// Object ID of the principal.
|
||||
ObjectIdKey = attribute.Key(contracts.UserAuthUserId) // user_AuthenticatedId
|
||||
// Tenant ID of the principal.
|
||||
TenantIdKey = attribute.Key("ad.tenant.id")
|
||||
// The type of account. See AccountTypeUser for all possible options.
|
||||
AccountTypeKey = attribute.Key("ad.account.type")
|
||||
// Currently selected Subscription ID.
|
||||
SubscriptionIdKey = attribute.Key("ad.subscription.id")
|
||||
)
|
||||
|
||||
// Project (azure.yaml) related attributes
|
||||
const (
|
||||
// Hashed template ID metadata
|
||||
ProjectTemplateIdKey = attribute.Key("project.template.id")
|
||||
// Hashed template.version metadata
|
||||
ProjectTemplateVersionKey = attribute.Key("project.template.version")
|
||||
// Hashed project name. Could be used as an indicator for number of different azd projects.
|
||||
ProjectNameKey = attribute.Key("project.name")
|
||||
// The collection of hashed service hosts in the project.
|
||||
ProjectServiceHostsKey = attribute.Key("project.service.hosts")
|
||||
// The collection of service targets (resolved service hosts) in the project.
|
||||
ProjectServiceTargetsKey = attribute.Key("project.service.targets")
|
||||
// The collection of hashed service languages in the project.
|
||||
ProjectServiceLanguagesKey = attribute.Key("project.service.languages")
|
||||
// The service language being executed.
|
||||
ProjectServiceLanguageKey = attribute.Key("project.service.language")
|
||||
)
|
||||
|
||||
// Platform related attributes for integrations like devcenter / ADE
|
||||
const (
|
||||
PlatformTypeKey = attribute.Key("platform.type")
|
||||
)
|
||||
|
||||
// Machine-level configuration related attribute.
|
||||
const (
|
||||
// Tracks what alpha features are enabled on each command
|
||||
AlphaFeaturesKey = attribute.Key("config.features")
|
||||
)
|
||||
|
||||
// Environment related attributes
|
||||
const (
|
||||
// Hashed environment name
|
||||
EnvNameKey = attribute.Key("env.name")
|
||||
)
|
||||
|
||||
// Command entry-point attributes
|
||||
const (
|
||||
// Flags set by the user. Only parsed flag names are available. Values are not recorded.
|
||||
CmdFlags = attribute.Key("cmd.flags")
|
||||
// Number of positional arguments set.
|
||||
CmdArgsCount = attribute.Key("cmd.args.count")
|
||||
// The command invocation entrypoint.
|
||||
//
|
||||
// The command invocation is formatted using [events.GetCommandEventName]. This makes it consistent with how
|
||||
// commands are represented in telemetry.
|
||||
CmdEntry = attribute.Key("cmd.entry")
|
||||
)
|
||||
|
||||
// All possible enumerations of ExecutionEnvironmentKey
|
||||
//
|
||||
// Environments are mutually exclusive. Modifiers can be set additionally to signal different types of usages.
|
||||
// An execution environment is formatted as follows:
|
||||
// `<environment>[;<modifier1>;<modifier2>...]`
|
||||
const (
|
||||
// A desktop environment. The user is directly interacting with azd via a terminal.
|
||||
EnvDesktop = "Desktop"
|
||||
|
||||
// Environments that are wrapped by an intermediate calling program, and are significant enough to warrant
|
||||
// being an environment and not an environment modifier.
|
||||
|
||||
EnvVisualStudio = "Visual Studio"
|
||||
EnvVisualStudioCode = "Visual Studio Code"
|
||||
EnvCloudShell = "Azure CloudShell"
|
||||
|
||||
// Continuous Integration environments
|
||||
|
||||
EnvUnknownCI = "UnknownCI"
|
||||
EnvAzurePipelines = "Azure Pipelines"
|
||||
EnvGitHubActions = "GitHub Actions"
|
||||
EnvAppVeyor = "AppVeyor"
|
||||
EnvBamboo = "Bamboo"
|
||||
EnvBitBucketPipelines = "BitBucket Pipelines"
|
||||
EnvTravisCI = "Travis CI"
|
||||
EnvCircleCI = "Circle CI"
|
||||
EnvGitLabCI = "GitLab CI"
|
||||
EnvJenkins = "Jenkins"
|
||||
EnvAwsCodeBuild = "AWS CodeBuild"
|
||||
EnvGoogleCloudBuild = "Google Cloud Build"
|
||||
EnvTeamCity = "TeamCity"
|
||||
EnvJetBrainsSpace = "JetBrains Space"
|
||||
EnvCodespaces = "GitHub Codespaces"
|
||||
|
||||
// Environment modifiers. These are not environments themselves, but rather modifiers to the environment
|
||||
// that signal specific types of usages.
|
||||
|
||||
EnvModifierAzureSpace = "Azure App Spaces Portal"
|
||||
)
|
||||
|
||||
// All possible enumerations of AccountTypeKey
|
||||
const (
|
||||
// A user.
|
||||
AccountTypeUser = "User"
|
||||
// A service principal, typically an application.
|
||||
AccountTypeServicePrincipal = "Service Principal"
|
||||
)
|
||||
|
||||
// The value used for ServiceNameKey
|
||||
const ServiceNameAzd = "azd"
|
||||
|
||||
// Error related fields
|
||||
const (
|
||||
// Error code that describes an error.
|
||||
ErrCode = attribute.Key("error.code")
|
||||
|
||||
// Inner error.
|
||||
ErrInner = attribute.Key("error.inner")
|
||||
|
||||
// The frame of the error.
|
||||
ErrFrame = attribute.Key("error.frame")
|
||||
)
|
||||
|
||||
// Service related fields.
|
||||
const (
|
||||
// Hostname of the service.
|
||||
// The list of allowed values can be found in [Domains].
|
||||
ServiceHost = attribute.Key("service.host")
|
||||
|
||||
// Name of the service.
|
||||
ServiceName = attribute.Key("service.name")
|
||||
|
||||
// Status code of a response returned by the service.
|
||||
// For HTTP, this corresponds to the HTTP status code.
|
||||
ServiceStatusCode = attribute.Key("service.statusCode")
|
||||
|
||||
// Method of a request to the service.
|
||||
// For HTTP, this corresponds to the HTTP method of the request made.
|
||||
ServiceMethod = attribute.Key("service.method")
|
||||
|
||||
// An error code returned by the service in a response.
|
||||
// For HTTP, the error code can be found in the response header or body.
|
||||
ServiceErrorCode = attribute.Key("service.errorCode")
|
||||
|
||||
// Correlation ID for a request to the service.
|
||||
ServiceCorrelationId = attribute.Key("service.correlationId")
|
||||
)
|
||||
|
||||
// Tool related fields
|
||||
const (
|
||||
// The name of the tool.
|
||||
ToolName = attribute.Key("tool.name")
|
||||
|
||||
// The exit code of the tool after invocation.
|
||||
ToolExitCode = attribute.Key("tool.exitCode")
|
||||
)
|
||||
|
||||
// Performance related fields
|
||||
const (
|
||||
// The time spent waiting on user interaction in milliseconds.
|
||||
PerfInteractTime = attribute.Key("perf.interact_time")
|
||||
)
|
||||
|
||||
// Pack related fields
|
||||
const (
|
||||
// The builder image used. Hashed when a user-defined image is used.
|
||||
PackBuilderImage = attribute.Key("pack.builder.image")
|
||||
|
||||
// The tag of the builder image used. Hashed when a user-defined image is used.
|
||||
PackBuilderTag = attribute.Key("pack.builder.tag")
|
||||
)
|
||||
|
||||
// Initialization from app related fields
|
||||
const (
|
||||
InitMethod = attribute.Key("init.method")
|
||||
|
||||
AppInitDetectedDatabase = attribute.Key("appinit.detected.databases")
|
||||
AppInitDetectedServices = attribute.Key("appinit.detected.services")
|
||||
|
||||
AppInitConfirmedDatabases = attribute.Key("appinit.confirmed.databases")
|
||||
AppInitConfirmedServices = attribute.Key("appinit.confirmed.services")
|
||||
|
||||
AppInitModifyAddCount = attribute.Key("appinit.modify_add.count")
|
||||
AppInitModifyRemoveCount = attribute.Key("appinit.modify_remove.count")
|
||||
|
||||
// The last step recorded during the app init process.
|
||||
AppInitLastStep = attribute.Key("appinit.lastStep")
|
||||
)
|
||||
|
||||
// Remote docker build related fields
|
||||
const (
|
||||
RemoteBuildCount = attribute.Key("container.remoteBuild.count")
|
||||
)
|
||||
|
||||
// JSON-RPC related fields
|
||||
const (
|
||||
// Logical name of the method from the RPC interface
|
||||
// perspective, which can be different from the name of any implementing
|
||||
// method/function. See semconv.RPCMethodKey.
|
||||
RpcMethod = semconv.RPCMethodKey
|
||||
|
||||
// `id` property of JSON-RPC request or response.
|
||||
JsonRpcId = semconv.RPCJsonrpcRequestIDKey
|
||||
|
||||
// `error_code` property of JSON-RPC request or response. Type: int.
|
||||
JsonRpcErrorCode = semconv.RPCJsonrpcErrorCodeKey
|
||||
)
|
|
@ -0,0 +1,48 @@
|
|||
package fields
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
func StringHashed(k attribute.Key, v string) attribute.KeyValue {
|
||||
return attribute.KeyValue{
|
||||
Key: attribute.Key(k),
|
||||
Value: attribute.StringValue(CaseInsensitiveHash(v)),
|
||||
}
|
||||
}
|
||||
|
||||
func StringSliceHashed(k attribute.Key, v []string) attribute.KeyValue {
|
||||
val := CaseInsensitiveSliceHash(v)
|
||||
return attribute.KeyValue{
|
||||
Key: attribute.Key(k),
|
||||
Value: attribute.StringSliceValue(val),
|
||||
}
|
||||
}
|
||||
|
||||
func CaseInsensitiveSliceHash(value []string) []string {
|
||||
hashed := make([]string, len(value))
|
||||
for i := range value {
|
||||
hashed[i] = CaseInsensitiveHash(value[i])
|
||||
}
|
||||
return hashed
|
||||
}
|
||||
|
||||
func CaseInsensitiveHash(value string) string {
|
||||
return Sha256Hash(strings.ToLower(value))
|
||||
}
|
||||
|
||||
// Sha256Hash returns the hex-encoded Sha256 hash of the given string.
|
||||
func Sha256Hash(val string) string {
|
||||
sha := sha256.Sum256([]byte(val))
|
||||
hash := hex.EncodeToString(sha[:])
|
||||
return hash
|
||||
}
|
||||
|
||||
// ErrorKey returns a new Key with "error." prefix appended.
|
||||
func ErrorKey(k attribute.Key) attribute.Key {
|
||||
return attribute.Key("error." + string(k))
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package resource
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/internal/tracing/fields"
|
||||
)
|
||||
|
||||
// Rules that apply when the specified environment variable is set to "true" (case-insensitive)
|
||||
var ciVarBoolRules = []struct {
|
||||
envVar string
|
||||
environment string
|
||||
}{
|
||||
// Azure Pipelines -
|
||||
// https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables#system-variables-devops-servicesQ
|
||||
{"TF_BUILD", fields.EnvAzurePipelines},
|
||||
// GitHub Actions,
|
||||
// https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables
|
||||
{"GITHUB_ACTIONS", fields.EnvGitHubActions},
|
||||
// AppVeyor - https://www.appveyor.com/docs/environment-variables/
|
||||
{"APPVEYOR", fields.EnvAppVeyor},
|
||||
// Travis CI - https://docs.travis-ci.com/user/environment-variables/#default-environment-variables
|
||||
{"TRAVIS", fields.EnvTravisCI},
|
||||
// Circle CI - https://circleci.com/docs/env-vars#built-in-environment-variables
|
||||
{"CIRCLECI", fields.EnvCircleCI},
|
||||
// GitLab CI
|
||||
{"GITLAB_CI", fields.EnvGitLabCI},
|
||||
}
|
||||
|
||||
// Rules that apply when the specified environment variable is set to any value
|
||||
var ciVarSetRules = []struct {
|
||||
envVar string
|
||||
environment string
|
||||
}{
|
||||
// AWS CodeBuild - https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-env-vars.html
|
||||
{"CODEBUILD_BUILD_ID", fields.EnvAwsCodeBuild},
|
||||
//nolint:lll
|
||||
// Jenkins -
|
||||
// https://github.com/jenkinsci/jenkins/blob/master/core/src/main/resources/jenkins/model/CoreEnvironmentContributor/buildEnv.groovy
|
||||
{"JENKINS_URL", fields.EnvJenkins},
|
||||
//nolint:lll
|
||||
// TeamCity - https://www.jetbrains.com/help/teamcity/predefined-build-parameters.html#Predefined+Server+Build+Parameters
|
||||
{"TEAMCITY_VERSION", fields.EnvTeamCity},
|
||||
//nolint:lll
|
||||
// JetBrains Space -
|
||||
// https://www.jetbrains.com/help/space/automation-environment-variables.html#when-does-automation-resolve-its-environment-variables
|
||||
{"JB_SPACE_API_URL", fields.EnvJetBrainsSpace},
|
||||
// Bamboo -
|
||||
// https://confluence.atlassian.com/bamboo/bamboo-variables-289277087.html#Bamboovariables-Build-specificvariables
|
||||
{"bamboo.buildKey", fields.EnvBamboo},
|
||||
// BitBucket - https://support.atlassian.com/bitbucket-cloud/docs/variables-and-secrets/
|
||||
{"BITBUCKET_BUILD_NUMBER", fields.EnvBitBucketPipelines},
|
||||
// Unknown CI cases
|
||||
{"CI", fields.EnvUnknownCI},
|
||||
{"BUILD_ID", fields.EnvUnknownCI},
|
||||
}
|
||||
|
||||
// getExecutionEnvironmentForHosted detects the execution environment for CI/CD providers and returns the corresponding
|
||||
// named environment.
|
||||
//
|
||||
// Returns an empty string if no CI/CD provider is detected.
|
||||
func execEnvForCi() string {
|
||||
for _, rule := range ciVarBoolRules {
|
||||
// Some CI providers specify 'True' on Windows vs 'true' on Linux, while others use `True` always
|
||||
// Thus, it's better to err on the side of being generous and be case-insensitive
|
||||
if strings.ToLower(os.Getenv(rule.envVar)) == "true" {
|
||||
return rule.environment
|
||||
}
|
||||
}
|
||||
|
||||
for _, rule := range ciVarSetRules {
|
||||
if _, ok := os.LookupEnv(rule.envVar); ok {
|
||||
return rule.environment
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// IsRunningOnCI returns true if the current process is running on a CI/CD provider.
|
||||
func IsRunningOnCI() bool {
|
||||
return execEnvForCi() != ""
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package resource
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/internal"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/internal/runcontext"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/internal/tracing/fields"
|
||||
)
|
||||
|
||||
func getExecutionEnvironment() string {
|
||||
// calling programs receive the highest priority, since they end up wrapping the CLI and are the most
|
||||
// inner layers.
|
||||
env := execEnvFromCaller()
|
||||
|
||||
if env == "" {
|
||||
// machine-level execution environments
|
||||
env = execEnvForHosts()
|
||||
}
|
||||
|
||||
if env == "" {
|
||||
// machine-level CI execution environments
|
||||
env = execEnvForCi()
|
||||
}
|
||||
|
||||
// no special execution environment found, default to plain desktop
|
||||
if env == "" {
|
||||
env = fields.EnvDesktop
|
||||
}
|
||||
|
||||
// global modifiers that are applicable to all environments
|
||||
modifiers := execEnvModifiers()
|
||||
|
||||
return strings.Join(append([]string{env}, modifiers...), ";")
|
||||
}
|
||||
|
||||
func execEnvFromCaller() string {
|
||||
userAgent := os.Getenv(internal.AzdUserAgentEnvVar)
|
||||
|
||||
if strings.Contains(userAgent, internal.VsCodeAgentPrefix) {
|
||||
return fields.EnvVisualStudioCode
|
||||
}
|
||||
|
||||
if strings.Contains(userAgent, internal.VsAgentPrefix) {
|
||||
return fields.EnvVisualStudio
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func execEnvForHosts() string {
|
||||
if _, ok := os.LookupEnv(runcontext.AzdInCloudShellEnvVar); ok {
|
||||
return fields.EnvCloudShell
|
||||
}
|
||||
|
||||
// GitHub Codespaces
|
||||
// https://docs.github.com/en/codespaces/developing-in-codespaces/default-environment-variables-for-your-codespacei
|
||||
if _, ok := os.LookupEnv("CODESPACES"); ok {
|
||||
return fields.EnvCodespaces
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func execEnvModifiers() []string {
|
||||
modifiers := []string{}
|
||||
userAgent := os.Getenv(internal.AzdUserAgentEnvVar)
|
||||
|
||||
if strings.Contains(userAgent, "azure_app_space_portal") {
|
||||
modifiers = append(modifiers, fields.EnvModifierAzureSpace)
|
||||
}
|
||||
|
||||
return modifiers
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package resource
|
||||
|
||||
import (
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/internal/installer"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/internal/tracing/fields"
|
||||
)
|
||||
|
||||
// Returns a hash of the content of `.installed-by.txt` file in the same directory as
|
||||
// the executable. If the file does not exist, returns empty string.
|
||||
func getInstalledBy() string {
|
||||
return fields.Sha256Hash(installer.RawInstalledBy())
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package resource
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
deviceid "github.com/microsoft/go-deviceid"
|
||||
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/common/permissions"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/config"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/internal/tracing/fields"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const machineIdCacheFileName = "machine-id.cache"
|
||||
|
||||
var invalidMacAddresses = map[string]struct{}{
|
||||
"00:00:00:00:00:00": {},
|
||||
"ff:ff:ff:ff:ff:ff": {},
|
||||
"ac:de:48:00:11:22": {},
|
||||
}
|
||||
|
||||
// DevDeviceId returns the unique device ID for the machine.
|
||||
func DevDeviceId() string {
|
||||
deviceId, err := deviceid.Get()
|
||||
|
||||
if err != nil {
|
||||
log.Println("could not get device id, returning empty: ", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
return deviceId
|
||||
}
|
||||
|
||||
// MachineId returns a unique ID for the machine.
|
||||
func MachineId() string {
|
||||
// We store the machine ID on the filesystem not due to performance,
|
||||
// but to increase the stability of the ID to be constant across factors like changing mac addresses, NICs.
|
||||
return loadOrCalculate(calculateMachineId, machineIdCacheFileName)
|
||||
}
|
||||
|
||||
func calculateMachineId() string {
|
||||
mac, ok := getMacAddress()
|
||||
|
||||
if ok {
|
||||
return fields.Sha256Hash(mac)
|
||||
} else {
|
||||
// No valid mac address, return a GUID instead.
|
||||
return uuid.NewString()
|
||||
}
|
||||
}
|
||||
|
||||
func loadOrCalculate(calc func() string, cacheFileName string) string {
|
||||
configDir, err := config.GetUserConfigDir()
|
||||
if err != nil {
|
||||
log.Printf("could not load machineId from cache. returning calculated value: %s", err)
|
||||
return calc()
|
||||
}
|
||||
|
||||
cacheFile := filepath.Join(configDir, cacheFileName)
|
||||
bytes, err := os.ReadFile(configDir)
|
||||
if err == nil {
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
err = os.WriteFile(cacheFile, []byte(calc()), permissions.PermissionFile)
|
||||
if err != nil {
|
||||
log.Printf("could not write machineId to cache. returning calculated value: %s", err)
|
||||
}
|
||||
|
||||
return calc()
|
||||
}
|
||||
|
||||
func getMacAddress() (string, bool) {
|
||||
interfaces, _ := net.Interfaces()
|
||||
for _, ift := range interfaces {
|
||||
if len(ift.HardwareAddr) > 0 && ift.Flags&net.FlagLoopback == 0 {
|
||||
hwAddr, err := net.ParseMAC(ift.HardwareAddr.String())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
ipAddr, _ := ift.Addrs()
|
||||
if len(ipAddr) == 0 || ift.Flags&net.FlagUp == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
mac := hwAddr.String()
|
||||
if isValidMacAddress(mac) {
|
||||
return mac, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
func isValidMacAddress(addr string) bool {
|
||||
_, invalidAddr := invalidMacAddresses[addr]
|
||||
return !invalidAddr
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
// Package resource provides application-level resource attributes for telemetry purposes.
|
||||
package resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/internal"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/internal/osversion"
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/internal/tracing/fields"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
|
||||
)
|
||||
|
||||
// New creates a resource with all application-level fields populated.
|
||||
func New() *resource.Resource {
|
||||
r, err := resource.Merge(
|
||||
resource.Default(),
|
||||
resource.NewWithAttributes(
|
||||
semconv.SchemaURL,
|
||||
fields.ServiceNameKey.String(fields.ServiceNameAzd),
|
||||
fields.ServiceVersionKey.String(internal.VersionInfo().Version.String()),
|
||||
fields.OSTypeKey.String(runtime.GOOS),
|
||||
fields.OSVersionKey.String(getOsVersion()),
|
||||
fields.HostArchKey.String(runtime.GOARCH),
|
||||
fields.ProcessRuntimeVersionKey.String(runtime.Version()),
|
||||
fields.ExecutionEnvironmentKey.String(getExecutionEnvironment()),
|
||||
fields.MachineIdKey.String(MachineId()),
|
||||
fields.InstalledByKey.String(getInstalledBy()),
|
||||
fields.DevDeviceIdKey.String(DevDeviceId()),
|
||||
),
|
||||
)
|
||||
|
||||
// One possible reason this might fail is if there's a mismatch between the semconv.SchemaURL and the schema used
|
||||
// by resource.Default(). This can happen if we upgrade our open telemetry package version but don't update the import
|
||||
// path of `semconv` above to point to the correct version. Instead of returning an empty resource without any attributes
|
||||
// just fail eagerly.
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to create resource: %v", err))
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func getOsVersion() string {
|
||||
ver, err := osversion.GetVersion()
|
||||
|
||||
if err != nil {
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
return ver
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/internal/tracing/baggage"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// It is often valuable to extend functionality of 3rd-party library types.
|
||||
// Therefore, we provide extension points for OpenTelemetry tracers here.
|
||||
|
||||
// Tracer is the creator of Spans.
|
||||
//
|
||||
// This is simply trace.Tracer except returning our Span instead of trace.Span.
|
||||
type Tracer interface {
|
||||
Start(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, Span)
|
||||
}
|
||||
|
||||
// Wrapper around trace.Tracer.
|
||||
type wrapperTracer struct {
|
||||
tracer trace.Tracer
|
||||
}
|
||||
|
||||
func (w *wrapperTracer) Start(
|
||||
ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, Span) {
|
||||
ctx, span := w.tracer.Start(ctx, spanName, opts...)
|
||||
// Propagate any baggage in the current context
|
||||
baggage := baggage.BaggageFromContext(ctx)
|
||||
span.SetAttributes(baggage.Attributes()...)
|
||||
return ctx, &wrapperSpan{span}
|
||||
}
|
||||
|
||||
// redefinedSpan is a slightly modified version of trace.Span.
|
||||
//
|
||||
// The only change made is to remove functionality around emitting events.
|
||||
// Events are nested telemetry events that can be fired from a Span.
|
||||
// We currently do not support this yet (no use case), but this is likely to be changed in the future.
|
||||
//
|
||||
// Exact modifications to trace.Span:
|
||||
// - Removed AddEvent
|
||||
// - Removed RecordError. This creates an error type event
|
||||
type redefinedSpan interface {
|
||||
// End completes the Span. The Span is considered complete and ready to be
|
||||
// delivered through the rest of the telemetry pipeline after this method
|
||||
// is called. Therefore, updates to the Span are not allowed after this
|
||||
// method has been called.
|
||||
End(options ...trace.SpanEndOption)
|
||||
|
||||
// IsRecording returns the recording state of the Span. It will return
|
||||
// true if the Span is active and events can be recorded.
|
||||
IsRecording() bool
|
||||
|
||||
// SpanContext returns the SpanContext of the Span. The returned SpanContext
|
||||
// is usable even after the End method has been called for the Span.
|
||||
SpanContext() trace.SpanContext
|
||||
|
||||
// SetStatus sets the status of the Span in the form of a code and a
|
||||
// description, overriding previous values set. The description is only
|
||||
// included in a status when the code is for an error.
|
||||
SetStatus(code codes.Code, description string)
|
||||
|
||||
// SetName sets the Span name.
|
||||
SetName(name string)
|
||||
|
||||
// SetAttributes sets kv as attributes of the Span. If a key from kv
|
||||
// already exists for an attribute of the Span it will be overwritten with
|
||||
// the value contained in kv.
|
||||
SetAttributes(kv ...attribute.KeyValue)
|
||||
|
||||
// TracerProvider returns a TracerProvider that can be used to generate
|
||||
// additional Spans on the same telemetry pipeline as the current Span.
|
||||
TracerProvider() trace.TracerProvider
|
||||
}
|
||||
|
||||
// Span is the individual component of a trace. It represents a single named
|
||||
// and timed operation of a workflow that is traced. A Tracer is used to
|
||||
// create a Span and it is then up to the operation the Span represents to
|
||||
// properly end the Span when the operation itself ends.
|
||||
type Span interface {
|
||||
redefinedSpan
|
||||
|
||||
// EndWithStatus calls End, but also sets Status based on the value of err (nil mean success).
|
||||
EndWithStatus(err error, options ...trace.SpanEndOption)
|
||||
}
|
||||
|
||||
type wrapperSpan struct {
|
||||
span trace.Span
|
||||
}
|
||||
|
||||
// End completes the Span. The Span is considered complete and ready to be
|
||||
// delivered through the rest of the telemetry pipeline after this method
|
||||
// is called. Therefore, updates to the Span are not allowed after this
|
||||
// method has been called.
|
||||
func (s *wrapperSpan) End(options ...trace.SpanEndOption) {
|
||||
s.span.SetAttributes(GetGlobalAttributes()...)
|
||||
s.span.End(options...)
|
||||
}
|
||||
|
||||
func (s *wrapperSpan) EndWithStatus(err error, options ...trace.SpanEndOption) {
|
||||
if err != nil {
|
||||
s.span.SetStatus(codes.Error, "UnknownError")
|
||||
} else {
|
||||
s.span.SetStatus(codes.Ok, "")
|
||||
}
|
||||
|
||||
s.End(options...)
|
||||
}
|
||||
|
||||
// IsRecording returns the recording state of the Span. It will return
|
||||
// true if the Span is active and events can be recorded.
|
||||
func (s *wrapperSpan) IsRecording() bool {
|
||||
return s.span.IsRecording()
|
||||
}
|
||||
|
||||
// SpanContext returns the SpanContext of the Span. The returned SpanContext
|
||||
// is usable even after the End method has been called for the Span.
|
||||
func (s *wrapperSpan) SpanContext() trace.SpanContext {
|
||||
return s.span.SpanContext()
|
||||
}
|
||||
|
||||
// SetStatus sets the status of the Span in the form of a code and a
|
||||
// description, overriding previous values set. The description is only
|
||||
// included in a status when the code is for an error.
|
||||
func (s *wrapperSpan) SetStatus(code codes.Code, description string) {
|
||||
s.span.SetStatus(code, description)
|
||||
}
|
||||
|
||||
// SetName sets the Span name.
|
||||
func (s *wrapperSpan) SetName(name string) {
|
||||
s.span.SetName(name)
|
||||
}
|
||||
|
||||
// SetAttributes sets kv as attributes of the Span. If a key from kv
|
||||
// already exists for an attribute of the Span it will be overwritten with
|
||||
// the value contained in kv.
|
||||
func (s *wrapperSpan) SetAttributes(kv ...attribute.KeyValue) {
|
||||
s.span.SetAttributes(kv...)
|
||||
}
|
||||
|
||||
// TracerProvider returns a TracerProvider that can be used to generate
|
||||
// additional Spans on the same telemetry pipeline as the current Span.
|
||||
func (s *wrapperSpan) TracerProvider() trace.TracerProvider {
|
||||
return s.span.TracerProvider()
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/internal/tracing/fields"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
var tracer = &wrapperTracer{otel.Tracer(fields.ServiceNameAzd)}
|
||||
|
||||
// Start creates a span and a context.Context containing the newly-created span.
|
||||
//
|
||||
// If the context.Context provided in `ctx` contains a Span then the newly-created
|
||||
// Span will be a child of that span, otherwise it will be a root span. This behavior
|
||||
// can be overridden by providing `WithNewRoot()` as a SpanOption, causing the
|
||||
// newly-created Span to be a root span even if `ctx` contains a Span.
|
||||
//
|
||||
// When creating a Span it is recommended to provide all known span attributes using
|
||||
// the `WithAttributes()` SpanOption as samplers will only have access to the
|
||||
// attributes provided when a Span is created.
|
||||
func Start(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, Span) {
|
||||
return tracer.Start(ctx, name, opts...)
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Environment variable that identifies a user agent calling into azd.
|
||||
// Any caller of azd can set this variable to identify themselves.
|
||||
const AzdUserAgentEnvVar = "AZURE_DEV_USER_AGENT"
|
||||
|
||||
// Well-known user agents prefixes.
|
||||
const (
|
||||
VsCodeAgentPrefix = "vscode:/extensions/ms-azuretools.azure-dev"
|
||||
// cspell: disable-next-line
|
||||
VsAgentPrefix = "vside:/webtools/azdev.publish"
|
||||
)
|
||||
|
||||
// UserAgent() creates the user agent string for azd.
|
||||
//
|
||||
// Examples:
|
||||
// - azdev/1.0.0 (Go 1.18; windows/amd64)
|
||||
// - azdev/1.0.0 (Go 1.18; windows/amd64) azd-caller/1.0.0
|
||||
func UserAgent() string {
|
||||
sb := strings.Builder{}
|
||||
sb.WriteString(fmt.Sprintf("azdev/%s", VersionInfo().Version.String()))
|
||||
sb.WriteString(" ")
|
||||
sb.WriteString(runtimeInfo())
|
||||
callerAgent := os.Getenv(AzdUserAgentEnvVar)
|
||||
if callerAgent != "" {
|
||||
sb.WriteString(" ")
|
||||
sb.WriteString(callerAgent)
|
||||
}
|
||||
|
||||
if strings.ToLower(os.Getenv("GITHUB_ACTIONS")) == "true" {
|
||||
sb.WriteString(" ")
|
||||
sb.WriteString("GhActions")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func runtimeInfo() string {
|
||||
return fmt.Sprintf("(Go %s; %s/%s)", runtime.Version(), runtime.GOOS, runtime.GOARCH)
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package internal
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
)
|
||||
|
||||
// devVersionString is the default version that is used when [Version] is not overridden at build time, i.e.
|
||||
// a developer building locally using `go install`.
|
||||
const devVersionString = "0.0.0-dev.0 (commit 0000000000000000000000000000000000000000)"
|
||||
|
||||
// The version string, as printed by `azd version`.
|
||||
//
|
||||
// This MUST be of the form "<semver> (commit <full commit hash>)"
|
||||
//
|
||||
// The default value here is used for a version built directly by a developer when running either
|
||||
// `go install` or `go build` without overriding the value at link time (the default behavior when
|
||||
// build or install are run without arguments).
|
||||
//
|
||||
// Official builds set this value based on the version and commit we are building, using `-ldflags`
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// -ldflags="-X 'github.com/azure/azure-dev/cli/sdk/azdcore/internal.Version=0.0.1-alpha.1 (commit 8a49ae5ae9ab13beeade35f91ad4b4611c2f5574)'"
|
||||
//
|
||||
// This value is exported and not const so it can be mutated by certain tests. Instead of accessing this member
|
||||
// directly, use [VersionInfo] which returns a structured version of this value.
|
||||
//
|
||||
// nolint: lll
|
||||
var Version = devVersionString
|
||||
|
||||
func init() {
|
||||
// VersionInfo panics if the version string is malformed, run the code at package startup to
|
||||
// ensure everything is okay. This allows the rest of the system to call VersionInfo() to get
|
||||
// parsed version information without having to worry about error handling.
|
||||
_ = VersionInfo()
|
||||
}
|
||||
|
||||
type AzdVersionInfo struct {
|
||||
Version semver.Version
|
||||
Commit string
|
||||
}
|
||||
|
||||
func IsDevVersion() bool {
|
||||
return Version == devVersionString
|
||||
}
|
||||
|
||||
func IsNonProdVersion() bool {
|
||||
if IsDevVersion() {
|
||||
return true
|
||||
}
|
||||
|
||||
// This currently relies on checking for specific internal release tags.
|
||||
// This can be improved to instead check for any presence of prerelease versioning
|
||||
// once the product is GA.
|
||||
return strings.Contains(VersionInfo().Version.String(), "pr")
|
||||
}
|
||||
|
||||
var versionStringRegexp = regexp.MustCompile(`^(\S+) \(commit ([0-9a-f]{40})\)$`)
|
||||
|
||||
func VersionInfo() AzdVersionInfo {
|
||||
matches := versionStringRegexp.FindStringSubmatch(Version)
|
||||
|
||||
if len(matches) != 3 {
|
||||
panic("azd version is malformed, ensure github.com/azure/azure-dev/cli/sdk/azdcore/internal.Version is correct")
|
||||
}
|
||||
|
||||
return AzdVersionInfo{
|
||||
Version: semver.MustParse(matches[1]),
|
||||
Commit: matches[2],
|
||||
}
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
package aspire
|
||||
|
||||
import (
|
||||
"github.com/azure/azure-dev/cli/sdk/azdcore/common"
|
||||
"github.com/psanford/memfs"
|
||||
)
|
||||
|
||||
type Manifest struct {
|
||||
Schema string `json:"$schema"`
|
||||
Resources map[string]*Resource `json:"resources"`
|
||||
// BicepFiles holds any bicep files generated by Aspire next to the manifest file.
|
||||
BicepFiles *memfs.FS `json:"-"`
|
||||
}
|
||||
|
||||
type Resource struct {
|
||||
// Type is present on all resource types
|
||||
Type string `json:"type"`
|
||||
|
||||
// Path is present on a project.v0 resource and is the path to the project file, and on a dockerfile.v0
|
||||
// resource and is the path to the Dockerfile (including the "Dockerfile" filename).
|
||||
// For a bicep.v0 resource, it is the path to the bicep file.
|
||||
Path *string `json:"path,omitempty"`
|
||||
|
||||
// Context is present on a dockerfile.v0 resource and is the path to the context directory.
|
||||
Context *string `json:"context,omitempty"`
|
||||
|
||||
// BuildArgs is present on a dockerfile.v0 resource and is the --build-arg for building the docker image.
|
||||
BuildArgs map[string]string `json:"buildArgs,omitempty"`
|
||||
|
||||
// Args is optionally present on project.v0 and dockerfile.v0 resources and are the arguments to pass to the container.
|
||||
Args []string `json:"args,omitempty"`
|
||||
|
||||
// Parent is present on a resource which is a child of another. It is the name of the parent resource. For example, a
|
||||
// postgres.database.v0 is a child of a postgres.server.v0, and so it would have a parent of which is the name of
|
||||
// the server resource.
|
||||
Parent *string `json:"parent,omitempty"`
|
||||
|
||||
// Image is present on a container.v0 resource and is the image to use for the container.
|
||||
Image *string `json:"image,omitempty"`
|
||||
|
||||
// Bindings is present on container.v0, project.v0 and dockerfile.v0 resources, and is a map of binding names to
|
||||
// binding details.
|
||||
Bindings common.WithOrder[Binding] `json:"bindings,omitempty"`
|
||||
|
||||
// Env is present on project.v0, container.v0 and dockerfile.v0 resources, and is a map of environment variable
|
||||
// names to value expressions. The value expressions are simple expressions like "{redis.connectionString}" or
|
||||
// "{postgres.port}" to allow referencing properties of other resources. The set of properties supported in these
|
||||
// expressions depends on the type of resource you are referencing.
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
|
||||
// Queues is optionally present on a azure.servicebus.v0 resource, and is a list of queue names to create.
|
||||
Queues *[]string `json:"queues,omitempty"`
|
||||
|
||||
// Topics is optionally present on a azure.servicebus.v0 resource, and is a list of topic names to create.
|
||||
Topics *[]string `json:"topics,omitempty"`
|
||||
|
||||
// Some resources just represent connections to existing resources that need not be provisioned. These resources have
|
||||
// a "connectionString" property which is the connection string that should be used during binding.
|
||||
ConnectionString *string `json:"connectionString,omitempty"`
|
||||
|
||||
// Dapr is present on dapr.v0 resources.
|
||||
Dapr *DaprResourceMetadata `json:"dapr,omitempty"`
|
||||
|
||||
// DaprComponent is present on dapr.component.v0 resources.
|
||||
DaprComponent *DaprComponentResourceMetadata `json:"daprComponent,omitempty"`
|
||||
|
||||
// Inputs is present on resources that need inputs from during the provisioning process (e.g asking for an API key, or
|
||||
// a password for a database).
|
||||
Inputs map[string]Input `json:"inputs,omitempty"`
|
||||
|
||||
// For a bicep.v0 resource, defines the input parameters for the bicep file.
|
||||
Params map[string]any `json:"params,omitempty"`
|
||||
|
||||
// parameter.v0 uses value field to define the value of the parameter.
|
||||
Value string
|
||||
|
||||
// container.v0 uses volumes field to define the volumes of the container.
|
||||
Volumes []*Volume `json:"volumes,omitempty"`
|
||||
|
||||
// The entrypoint to use for the container image when executed.
|
||||
Entrypoint string `json:"entrypoint,omitempty"`
|
||||
|
||||
// An object that captures properties that control the building of a container image.
|
||||
Build *ContainerV1Build `json:"build,omitempty"`
|
||||
|
||||
// container.v0 uses bind mounts field to define the volumes with initial data of the container.
|
||||
BindMounts []*BindMount `json:"bindMounts,omitempty"`
|
||||
}
|
||||
|
||||
type ContainerV1Build struct {
|
||||
// The path to the context directory for the container build.
|
||||
// Can be relative of absolute. If relative it is relative to the location of the manifest file.
|
||||
Context string `json:"context"`
|
||||
|
||||
// The path to the Dockerfile. Can be relative or absolute. If relative it is relative to the manifest file.
|
||||
Dockerfile string `json:"dockerfile"`
|
||||
|
||||
// Args is optionally present on project.v0 and dockerfile.v0 resources and are the arguments to pass to the container.
|
||||
Args map[string]string `json:"args,omitempty"`
|
||||
|
||||
// A list of build arguments which are used during container build."
|
||||
Secrets map[string]ContainerV1BuildSecrets `json:"secrets,omitempty"`
|
||||
}
|
||||
|
||||
type ContainerV1BuildSecrets struct {
|
||||
// "env" (will come with value) or "file" (will come with source).
|
||||
Type string `json:"type"`
|
||||
// If provided use as the value for the environment variable when docker build is run.
|
||||
Value *string `json:"value,omitempty"`
|
||||
// Path to secret file. If relative, the path is relative to the manifest file.
|
||||
Source *string `json:"source,omitempty"`
|
||||
}
|
||||
|
||||
type DaprResourceMetadata struct {
|
||||
AppId *string `json:"appId,omitempty"`
|
||||
Application *string `json:"application,omitempty"`
|
||||
AppPort *int `json:"appPort,omitempty"`
|
||||
AppProtocol *string `json:"appProtocol,omitempty"`
|
||||
DaprHttpMaxRequestSize *int `json:"daprHttpMaxRequestSize,omitempty"`
|
||||
DaprHttpReadBufferSize *int `json:"daprHttpReadBufferSize,omitempty"`
|
||||
EnableApiLogging *bool `json:"enableApiLogging,omitempty"`
|
||||
LogLevel *string `json:"logLevel,omitempty"`
|
||||
}
|
||||
|
||||
type DaprComponentResourceMetadata struct {
|
||||
Type *string `json:"type"`
|
||||
}
|
||||
|
||||
type Reference struct {
|
||||
Bindings []string `json:"bindings,omitempty"`
|
||||
}
|
||||
|
||||
type Binding struct {
|
||||
TargetPort *int `json:"targetPort,omitempty"`
|
||||
Port *int `json:"port,omitempty"`
|
||||
Scheme string `json:"scheme"`
|
||||
Protocol string `json:"protocol"`
|
||||
Transport string `json:"transport"`
|
||||
External bool `json:"external"`
|
||||
}
|
||||
|
||||
type Volume struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Target string `json:"target"`
|
||||
ReadOnly bool `json:"readOnly"`
|
||||
}
|
||||
|
||||
type BindMount struct {
|
||||
Name string `json:"-"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Target string `json:"target"`
|
||||
ReadOnly bool `json:"readOnly"`
|
||||
}
|
||||
|
||||
type Input struct {
|
||||
Type string `json:"type"`
|
||||
Secret bool `json:"secret"`
|
||||
Default *InputDefault `json:"default,omitempty"`
|
||||
}
|
||||
|
||||
type InputDefaultGenerate struct {
|
||||
MinLength *uint `json:"minLength,omitempty"`
|
||||
Lower *bool `json:"lower,omitempty"`
|
||||
Upper *bool `json:"upper,omitempty"`
|
||||
Numeric *bool `json:"numeric,omitempty"`
|
||||
Special *bool `json:"special,omitempty"`
|
||||
MinLower *uint `json:"minLower,omitempty"`
|
||||
MinUpper *uint `json:"minUpper,omitempty"`
|
||||
MinNumeric *uint `json:"minNumeric,omitempty"`
|
||||
MinSpecial *uint `json:"minSpecial,omitempty"`
|
||||
}
|
||||
|
||||
type InputDefault struct {
|
||||
Generate *InputDefaultGenerate `json:"generate,omitempty"`
|
||||
}
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче