diff --git a/go.mod b/go.mod index a02e1b7..6cd386a 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,11 @@ module github.com/nduyphuong/gorya go 1.20 require ( + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.8.0-beta.1 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.2 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v4 v4.2.1 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.2.0 github.com/aws/aws-sdk-go-v2 v1.21.0 github.com/aws/aws-sdk-go-v2/config v1.18.35 github.com/aws/aws-sdk-go-v2/credentials v1.13.34 @@ -20,7 +25,6 @@ require ( github.com/stretchr/testify v1.8.4 golang.org/x/net v0.14.0 golang.org/x/oauth2 v0.11.0 - golang.org/x/sync v0.2.0 google.golang.org/api v0.126.0 gorm.io/datatypes v1.2.0 gorm.io/driver/mysql v1.5.1 @@ -31,6 +35,8 @@ require ( require ( cloud.google.com/go/compute v1.20.1 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v0.9.0 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.10 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35 // indirect @@ -44,6 +50,7 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-cmp v0.5.9 // indirect @@ -55,8 +62,10 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/mattn/go-sqlite3 v1.14.17 // indirect github.com/nxadm/tail v1.4.8 // indirect + github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pquerna/cachecontrol v0.2.0 // indirect github.com/spf13/pflag v1.0.5 // indirect diff --git a/go.sum b/go.sum index a0c6231..0a32338 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,22 @@ cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZN cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.8.0-beta.1 h1:8t6ZZtkOCl+rx7uBn40Nj62ABVGkXK69U/En44wJIlE= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.8.0-beta.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.2 h1:uqM+VoHjVH6zdlkLF2b6O0ZANcHoj3rO0PoQ3jglUJA= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.2/go.mod h1:twTKAa1E6hLmSDjLhaCkbTMQKc7p/rNLU40rLxGEOCI= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v4 v4.2.1 h1:UPeCRD+XY7QlaGQte2EVI2iOcWvUYA2XY8w5T/8v0NQ= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v4 v4.2.1/go.mod h1:oGV6NlB0cvi1ZbYRR2UN44QHxWFyGk+iylgD0qaMXjA= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.1.2 h1:mLY+pNLjCUeKhgnAJWAKhEUQM+RJQo2H1fuGSw1Ky1E= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.0.0 h1:nBy98uKOIfun5z6wx6jwWLrULcM0+cjBalBFZlEZ7CA= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0 h1:ECsQtyERDVz3NP3kvDOTLvbQhqWp/x9EsGKtb4ogUr8= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0/go.mod h1:s1tW/At+xHqjNFvWU4G0c0Qv33KOhvbGNj0RCTQDV8s= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.2.0 h1:Pmy0+3ox1IC3sp6musv87BFPIdQbqyPFjn7I8I0o2Js= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.2.0/go.mod h1:ThfyMjs6auYrWPnYJjI3H4H++oVPrz01pizpu8lfl3A= +github.com/AzureAD/microsoft-authentication-library-for-go v0.9.0 h1:UE9n9rkJF62ArLb1F3DEjRt8O3jLwMWdSoypKV4f3MU= +github.com/AzureAD/microsoft-authentication-library-for-go v0.9.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/aws/aws-sdk-go-v2 v1.20.3/go.mod h1:/RfNgGmRxI+iFOB1OeJUyxiU+9s88k3pfHvDagGEp0M= @@ -57,6 +73,7 @@ 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/dnaeon/go-vcr v1.1.0 h1:ReYa/UBrRyQdant9B4fNHGoCNKw6qh6P0fsdGmZpR7c= 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= @@ -74,6 +91,8 @@ github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -134,6 +153,8 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +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-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE= @@ -149,6 +170,8 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -223,7 +246,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= -golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -237,6 +259,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/api/api.go b/internal/api/api.go index 08bd941..12867df 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -2,6 +2,9 @@ package api import ( "context" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions" + "github.com/nduyphuong/gorya/pkg/azure" "net" "net/http" "strings" @@ -10,7 +13,7 @@ import ( "github.com/nduyphuong/gorya/internal/api/config" "github.com/nduyphuong/gorya/internal/api/handler" - constants "github.com/nduyphuong/gorya/internal/constants" + "github.com/nduyphuong/gorya/internal/constants" "github.com/nduyphuong/gorya/internal/logging" "github.com/nduyphuong/gorya/internal/os" queueOptions "github.com/nduyphuong/gorya/internal/queue/options" @@ -22,6 +25,7 @@ import ( awsOptions "github.com/nduyphuong/gorya/pkg/aws/options" "github.com/nduyphuong/gorya/pkg/gcp" gcpOptions "github.com/nduyphuong/gorya/pkg/gcp/options" + "github.com/pkg/errors" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" @@ -36,6 +40,7 @@ type server struct { sc store.Interface aws *aws.ClientPool gcp *gcp.ClientPool + azure *azure.ClientPool taskProcessor worker.Interface } @@ -90,7 +95,6 @@ func (s *server) Serve(ctx context.Context, l net.Listener) error { } } } - // c.credentialRef["arn:aws:iam::043159268388:role/test"] = true s.aws, err = aws.NewPool( ctx, c.credentialRef, @@ -133,12 +137,12 @@ func (s *server) Serve(ctx context.Context, l net.Listener) error { } } } - c.credentialRef["priv-sa@target-project-397310.iam.gserviceaccount.com"] = true + //c.credentialRef["priv-sa@target-project-397310.iam.gserviceaccount.com"] = true s.gcp, err = gcp.NewPool( ctx, c.credentialRef, - gcpOptions.WithImpersonatedServiceAccountEmail(os.GetEnv("GCP_IMPERSONATED_SERVICE_ACCOUNT", "")), - gcpOptions.WithProject(os.GetEnv("GCP_PROJECT_ID", "")), + gcpOptions.WithImpersonatedServiceAccountEmail(os.MustGetEnv("GCP_IMPERSONATED_SERVICE_ACCOUNT")), + gcpOptions.WithProject(os.MustGetEnv("GCP_PROJECT_ID")), ) if err != nil { log.Errorf("update gcp client pool %v", err) @@ -158,7 +162,50 @@ func (s *server) Serve(ctx context.Context, l net.Listener) error { } }(ctx.Done()) } - + if provider == constants.PROVIDER_AZURE { + updateAzureClientPool := func() { + c.lock.Lock() + defer c.lock.Unlock() + conn, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + log.Fatalf("get default az credential %v", err) + } + armSubscription, err := armsubscriptions.NewClient(conn, nil) + if err != nil { + log.Fatalf("get armsubscriptions client %v", err) + } + subPager := armSubscription.NewListPager(nil) + for subPager.More() { + page, err := subPager.NextPage(ctx) + if err != nil { + log.Fatalf("list subscriptions %v", err) + } + for _, subscription := range page.Value { + c.credentialRef[*subscription.SubscriptionID] = true + } + } + s.azure, err = azure.NewPool( + ctx, + c.credentialRef, + ) + if err != nil { + log.Errorf("update azure client pool %v", err) + return + } + } + updateAzureClientPool() + go func(stop <-chan struct{}) { + for { + select { + case <-stop: + log.Info("shut down azure client pool") + return + case <-ticker.C: + updateAzureClientPool() + } + } + }(ctx.Done()) + } } s.taskProcessor = worker.NewClient(worker.Options{ @@ -229,7 +276,7 @@ func (s *server) DeletePolicy(ctx context.Context) http.HandlerFunc { } func (s *server) ChangeState(ctx context.Context) http.HandlerFunc { - return handler.ChangeStateV1alpha1(ctx, s.aws, s.gcp) + return handler.ChangeStateV1alpha1(ctx, s.aws, s.gcp, s.azure) } func (s *server) ScheduleTask(ctx context.Context) http.HandlerFunc { diff --git a/internal/api/handler/change_state_v1alpha1.go b/internal/api/handler/change_state_v1alpha1.go index 9328abe..d41f917 100644 --- a/internal/api/handler/change_state_v1alpha1.go +++ b/internal/api/handler/change_state_v1alpha1.go @@ -4,9 +4,10 @@ import ( "context" "encoding/json" "fmt" + "github.com/nduyphuong/gorya/pkg/azure" "net/http" - constants "github.com/nduyphuong/gorya/internal/constants" + "github.com/nduyphuong/gorya/internal/constants" svcv1alpha1 "github.com/nduyphuong/gorya/pkg/api/service/v1alpha1" "github.com/nduyphuong/gorya/pkg/aws" "github.com/nduyphuong/gorya/pkg/gcp" @@ -25,7 +26,9 @@ AssumeRoleProvider (k<=N) Optimization: - We can init a client pool identified by AssumeRoleARN */ -func ChangeStateV1alpha1(ctx context.Context, awsClientPool *aws.ClientPool, gcpClientPool *gcp.ClientPool) http.HandlerFunc { +func ChangeStateV1alpha1(ctx context.Context, awsClientPool *aws.ClientPool, gcpClientPool *gcp.ClientPool, + azureClientPool *azure.ClientPool, +) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { m := svcv1alpha1.ChangeStateRequest{} if err := json.NewDecoder(req.Body).Decode(&m); err != nil { @@ -47,6 +50,19 @@ func ChangeStateV1alpha1(ctx context.Context, awsClientPool *aws.ClientPool, gcp return } } + case constants.PROVIDER_AZURE: + if azureClientPool != nil { + azClient, ok := azureClientPool.GetForCredential(m.CredentialRef) + if !ok { + http.Error(w, fmt.Errorf("client not found for credential %v", m.CredentialRef).Error(), + http.StatusBadRequest) + return + } + compute := azClient.AVM() + if err := compute.ChangeStatus(ctx, m.Action, m.TagKey, m.TagValue); err != nil { + http.Error(w, pkgerrors.Wrap(err, "change compute status").Error(), http.StatusInternalServerError) + } + } case constants.PROVIDER_GCP: if gcpClientPool != nil { gcpClient, ok := gcpClientPool.GetForCredential(m.CredentialRef) diff --git a/internal/logging/context.go b/internal/logging/context.go index 93b0305..cd5fec8 100644 --- a/internal/logging/context.go +++ b/internal/logging/context.go @@ -2,6 +2,9 @@ package logging import ( "context" + "path" + "runtime" + "strconv" log "github.com/sirupsen/logrus" @@ -19,6 +22,14 @@ func init() { panic(err) } globalLogger.Logger.SetLevel(level) + globalLogger.Logger.SetReportCaller(true) + globalLogger.Logger.SetFormatter(&log.TextFormatter{ + CallerPrettyfier: func(frame *runtime.Frame) (function string, file string) { + fileName := path.Base(frame.File) + ":" + strconv.Itoa(frame.Line) + //return frame.Function, fileName + return "", fileName + }, + }) } // ContextWithLogger returns a context.Context that has been augmented with diff --git a/internal/os/env.go b/internal/os/env.go index 33131d8..7ea085d 100644 --- a/internal/os/env.go +++ b/internal/os/env.go @@ -1,6 +1,7 @@ package os import ( + "fmt" "os" ) @@ -13,3 +14,12 @@ func GetEnv(key, defaultValue string) string { } return value } + +// MustGetEnv retrieves the value of an environment variable having the specified key, panic if key not set +func MustGetEnv(key string) string { + value := os.Getenv(key) + if value == "" { + panic(fmt.Sprintf("%v not set", key)) + } + return value +} diff --git a/pkg/aws/client.go b/pkg/aws/client.go index c298184..e82a904 100644 --- a/pkg/aws/client.go +++ b/pkg/aws/client.go @@ -72,7 +72,7 @@ func new(ctx context.Context, opts ...options.Option) (*client, error) { } awsEndpoint := c.opts.AwsEndpoint awsRegion := c.opts.AwsRegion - // custom resolver so we can testing locally with localstack + // custom resolver so we can test locally with localstack customResolverWithOptions := aws.EndpointResolverWithOptionsFunc( func(service, region string, options ...interface{}) (aws.Endpoint, error) { if awsEndpoint != "" { diff --git a/pkg/azure/avm/client.go b/pkg/azure/avm/client.go new file mode 100644 index 0000000..2b8d599 --- /dev/null +++ b/pkg/azure/avm/client.go @@ -0,0 +1,89 @@ +package avm + +import ( + "context" + "errors" + "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v4" + "github.com/nduyphuong/gorya/internal/constants" + "github.com/nduyphuong/gorya/internal/logging" + "github.com/nduyphuong/gorya/pkg/azure/options" +) + +var ErrInvalidResourceStatus = errors.New("invalid resource status") + +type client struct { + avm *armcompute.VirtualMachinesClient + opts options.Options +} +type Interface interface { + ChangeStatus(ctx context.Context, to int, tagKey string, tagValue string) (err error) +} + +func New(conn azcore.TokenCredential, opts ...options.Option) (Interface, error) { + c := &client{} + for _, o := range opts { + o.Apply(&c.opts) + } + computeClientFactory, err := armcompute.NewClientFactory(c.opts.SubscriptionId, conn, nil) + c.avm = computeClientFactory.NewVirtualMachinesClient() + if err != nil { + return nil, err + } + return c, nil +} + +func (c *client) ChangeStatus(ctx context.Context, to int, tagKey string, tagValue string) (err error) { + logger := logging.LoggerFromContext(ctx) + if to != constants.OffStatus && to != constants.OnStatus { + return ErrInvalidResourceStatus + } + fmt.Printf("c.opts.TargetResourceGroups: %v\n", c.opts.TargetResourceGroups) + for _, rg := range c.opts.TargetResourceGroups { + vmPager := c.avm.NewListPager(rg, &armcompute.VirtualMachinesClientListOptions{}) + for vmPager.More() { + page, err := vmPager.NextPage(ctx) + if err != nil { + return err + } + for _, item := range page.Value { + if item.Tags == nil { + continue + } + if *item.Tags[tagKey] != tagValue { + continue + } + switch to { + case constants.OffStatus: + if err = c.turnOffInstance(ctx, *item.Name, rg); err != nil { + logger.Errorf("turn off avm %s in resource group %s %v", *item.Name, rg, err) + continue + } + logger.Infof("turned off avm %s in resource group %s", *item.Name, rg) + case constants.OnStatus: + if err = c.turnOnInstance(ctx, *item.Name, rg); err != nil { + logger.Errorf("turn on avm %s in resource group %s %v", *item.Name, rg, err) + continue + } + logger.Infof("turned on avm %s in resource group %s", *item.Name, rg) + } + } + } + } + return nil +} + +func (c *client) turnOnInstance(ctx context.Context, name string, resourceGroupName string) error { + if _, err := c.avm.BeginStart(ctx, resourceGroupName, name, nil); err != nil { + return err + } + return nil +} + +func (c *client) turnOffInstance(ctx context.Context, name string, resourceGroupName string) error { + if _, err := c.avm.BeginPowerOff(ctx, resourceGroupName, name, nil); err != nil { + return err + } + return nil +} diff --git a/pkg/azure/client.go b/pkg/azure/client.go new file mode 100644 index 0000000..826b488 --- /dev/null +++ b/pkg/azure/client.go @@ -0,0 +1,101 @@ +package azure + +import ( + "context" + "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources" + "github.com/nduyphuong/gorya/internal/constants" + "github.com/nduyphuong/gorya/pkg/azure/avm" + "github.com/nduyphuong/gorya/pkg/azure/options" + "sync" +) + +type Interface interface { + AVM() avm.Interface +} + +type ClientPool struct { + credToClient map[string]Interface +} + +type client struct { + avm avm.Interface + opts options.Options +} + +var ( + lock sync.Mutex +) + +// NewPool return a pool of client identified by subscriptionId +func NewPool(ctx context.Context, credentialRefs map[string]bool, + opts ...options.Option) (*ClientPool, + error) { + lock.Lock() + defer lock.Unlock() + b := &ClientPool{ + credToClient: make(map[string]Interface), + } + conn, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + return nil, err + } + + for subscription := range credentialRefs { + //cred = subscription + if _, ok := b.credToClient[subscription]; !ok { + resourceGroupClient, err := armresources.NewResourceGroupsClient(subscription, conn, nil) + if err != nil { + return nil, err + } + rPager := resourceGroupClient.NewListPager(nil) + var resourceGroups []string + for rPager.More() { + page, err := rPager.NextPage(ctx) + if err != nil { + return nil, err + } + for _, resourceGroup := range page.Value { + resourceGroups = append(resourceGroups, *resourceGroup.Name) + } + } + c, err := new(conn, append(opts, options.WithSubscriptionId(subscription), options.WithTargetResourceGroups(resourceGroups))...) + if err != nil { + return nil, err + } + b.credToClient[subscription] = c + } + } + return b, nil +} + +// new return a client for a subscriptionId +func new(conn azcore.TokenCredential, opts ...options.Option) (*client, error) { + avm, err := avm.New(conn, opts...) + if err != nil { + return nil, err + } + c := client{ + avm: avm, + } + if err != nil { + return nil, err + } + return &c, nil +} + +func (b *ClientPool) GetForCredential(name string) (Interface, bool) { + if name == constants.Default { + panic("az subscription id must not be empty") + } + i, ok := b.credToClient[name] + if !ok { + return nil, false + } + fmt.Printf("got client from pool for %s\n", name) + return i, true +} + +func (c *client) AVM() avm.Interface { return c.avm } diff --git a/pkg/azure/client_test.go b/pkg/azure/client_test.go new file mode 100644 index 0000000..6ae41cc --- /dev/null +++ b/pkg/azure/client_test.go @@ -0,0 +1,58 @@ +package azure + +import ( + "context" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions" + "github.com/nduyphuong/gorya/internal/os" + "github.com/stretchr/testify/assert" + "testing" +) + +/* +* +https://cloudstudio.com.au/2021/05/29/account-structure-aws-vs-azure/ +* +*/ + +type TestData struct { + TenantId string + SubscriptionId string + TargetVMName string + ClientId string + ClientSecret string + TargetTag struct { + Key, Value string + } +} + +func TestNewPool(t *testing.T) { + ctx := context.TODO() + d := TestData{ + TenantId: os.MustGetEnv("AZURE_TENANT_ID"), + SubscriptionId: os.MustGetEnv("AZURE_TARGET_SUBSCRIPTION_ID"), + ClientId: os.MustGetEnv("AZURE_CLIENT_ID"), + ClientSecret: os.MustGetEnv("AZURE_CLIENT_SECRET"), + TargetVMName: "test-vm", + TargetTag: struct{ Key, Value string }{Key: "foo", Value: "bar"}, + } + + conn, err := azidentity.NewDefaultAzureCredential(nil) + armSubscription, err := armsubscriptions.NewClient(conn, nil) + assert.NoError(t, err) + subPager := armSubscription.NewListPager(nil) + credentialRef := map[string]bool{} + for subPager.More() { + page, err := subPager.NextPage(ctx) + assert.NoError(t, err) + for _, subscription := range page.Value { + credentialRef[*subscription.SubscriptionID] = true + } + } + azurePool, err := NewPool(ctx, credentialRef) + assert.NoError(t, err) + azClient, exist := azurePool.GetForCredential(d.SubscriptionId) + assert.True(t, exist) + err = azClient.AVM().ChangeStatus(ctx, 0, d.TargetTag.Key, d.TargetTag.Value) + assert.NoError(t, err) +} diff --git a/pkg/azure/options/options.go b/pkg/azure/options/options.go new file mode 100644 index 0000000..3d1e4e7 --- /dev/null +++ b/pkg/azure/options/options.go @@ -0,0 +1,36 @@ +package options + +type Options struct { + //target SubscriptionId + SubscriptionId string + //target TargetResourceGroups + TargetResourceGroups []string +} + +type Option interface { + Apply(*Options) +} + +type subscriptionId string + +func (o subscriptionId) Apply(i *Options) { + if o != "" { + i.SubscriptionId = string(o) + } +} + +func WithSubscriptionId(d string) Option { + return subscriptionId(d) +} + +type targetResourceGroups []string + +func (o targetResourceGroups) Apply(i *Options) { + if o != nil { + i.TargetResourceGroups = []string(o) + } +} + +func WithTargetResourceGroups(d []string) Option { + return targetResourceGroups(d) +}