mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-01-12 16:09:39 -05:00
[node operator] Add Azure client
Signed-off-by: Malte Poll <mp@edgeless.systems>
This commit is contained in:
parent
a50cc2b64d
commit
c74360bf62
@ -3,6 +3,8 @@ module github.com/edgelesssys/constellation/operators/constellation-node-operato
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2 v2.0.0
|
||||
github.com/googleapis/gax-go/v2 v2.4.0
|
||||
github.com/medik8s/node-maintenance-operator v0.13.0
|
||||
github.com/onsi/ginkgo v1.16.5
|
||||
@ -18,12 +20,17 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1 // indirect
|
||||
github.com/coreos/etcd v3.3.13+incompatible // indirect
|
||||
github.com/coreos/go-semver v0.3.0 // indirect
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect
|
||||
github.com/golang-jwt/jwt v3.2.1+incompatible // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.1.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.4 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
@ -80,7 +87,7 @@ require (
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0
|
||||
go.uber.org/zap v1.19.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88 // indirect
|
||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2 // indirect
|
||||
golang.org/x/sys v0.0.0-20220624220833-87e55d714810 // indirect
|
||||
|
@ -59,6 +59,16 @@ cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.1 h1:tz19qLF65vuu2ibfTqGVJxG/zZAI27NEIIbvAOQwYbw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.1/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0 h1:QkAcEIAKbNL4KoFr4SathZPhDhF4mVwpBMFlYjyAqy8=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0/go.mod h1:bhXu1AjYL+wutSL/kpSq6s7733q2Rb0yuot9Zgfqa/0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0 h1:jp0dGvZ7ZK0mgqnTSClMxa5xuRL7NZgHameVYF6BurY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute v1.0.0 h1:/Di3vB4sNeQ+7A8efjUVENvyB945Wruvstucqp7ZArg=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2 v2.0.0 h1:xxe4naFUPYEW1W6C8yWrfFNmyZLnEbO+CsbsSF83wDo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2 v2.0.0/go.mod h1:aLFjumYDvv63tH1qnqkcmdjdZ6Sn+/viPv7H3jft0oY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.0.0 h1:lMW1lD/17LUA5z1XTURo7LcVG2ICBPlyMHjIUrcFZNQ=
|
||||
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/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
@ -74,6 +84,8 @@ github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+Z
|
||||
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
|
||||
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1 h1:BWe8a+f/t+7KY7zH2mqygeUD0t8hNFXe08p1Pb3/jKE=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4=
|
||||
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/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
|
||||
@ -154,6 +166,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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/dnaeon/go-vcr v1.1.0 h1:ReYa/UBrRyQdant9B4fNHGoCNKw6qh6P0fsdGmZpR7c=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||
@ -219,6 +232,9 @@ github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zV
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c=
|
||||
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@ -300,6 +316,7 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe
|
||||
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=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
@ -381,6 +398,8 @@ 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/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
@ -415,6 +434,7 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
@ -447,6 +467,8 @@ github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFSt
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 h1:Qj1ukM4GlMWXNdMBuXcXfz/Kw9s1qm0CLY32QxuSImI=
|
||||
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@ -604,8 +626,9 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE=
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88 h1:Tgea0cVUD0ivh5ADBX4WwuI12DUd2to3nCYe2eayMIw=
|
||||
golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
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=
|
||||
|
@ -0,0 +1,37 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
|
||||
armcomputev2 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
|
||||
"github.com/edgelesssys/constellation/operators/constellation-node-operator/internal/poller"
|
||||
)
|
||||
|
||||
type virtualMachineScaleSetVMsAPI interface {
|
||||
Get(ctx context.Context, resourceGroupName string, vmScaleSetName string, instanceID string,
|
||||
options *armcomputev2.VirtualMachineScaleSetVMsClientGetOptions,
|
||||
) (armcomputev2.VirtualMachineScaleSetVMsClientGetResponse, error)
|
||||
GetInstanceView(ctx context.Context, resourceGroupName string, vmScaleSetName string, instanceID string,
|
||||
options *armcomputev2.VirtualMachineScaleSetVMsClientGetInstanceViewOptions,
|
||||
) (armcomputev2.VirtualMachineScaleSetVMsClientGetInstanceViewResponse, error)
|
||||
NewListPager(resourceGroupName string, virtualMachineScaleSetName string,
|
||||
options *armcomputev2.VirtualMachineScaleSetVMsClientListOptions,
|
||||
) *runtime.Pager[armcomputev2.VirtualMachineScaleSetVMsClientListResponse]
|
||||
}
|
||||
|
||||
type scaleSetsAPI interface {
|
||||
Get(ctx context.Context, resourceGroupName string, vmScaleSetName string,
|
||||
options *armcomputev2.VirtualMachineScaleSetsClientGetOptions,
|
||||
) (armcomputev2.VirtualMachineScaleSetsClientGetResponse, error)
|
||||
BeginUpdate(ctx context.Context, resourceGroupName string, vmScaleSetName string, parameters armcomputev2.VirtualMachineScaleSetUpdate,
|
||||
options *armcomputev2.VirtualMachineScaleSetsClientBeginUpdateOptions,
|
||||
) (*runtime.Poller[armcomputev2.VirtualMachineScaleSetsClientUpdateResponse], error)
|
||||
BeginDeleteInstances(ctx context.Context, resourceGroupName string, vmScaleSetName string, vmInstanceIDs armcomputev2.VirtualMachineScaleSetVMInstanceRequiredIDs,
|
||||
options *armcomputev2.VirtualMachineScaleSetsClientBeginDeleteInstancesOptions,
|
||||
) (*runtime.Poller[armcomputev2.VirtualMachineScaleSetsClientDeleteInstancesResponse], error)
|
||||
}
|
||||
|
||||
type capacityPoller interface {
|
||||
PollUntilDone(context.Context, *poller.PollUntilDoneOptions) (int64, error)
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
|
||||
armcomputev2 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
|
||||
"github.com/edgelesssys/constellation/operators/constellation-node-operator/internal/poller"
|
||||
)
|
||||
|
||||
// Client is a client for the Azure Cloud.
|
||||
type Client struct {
|
||||
scaleSetsAPI
|
||||
virtualMachineScaleSetVMsAPI
|
||||
capacityPollerGenerator func(resourceGroup, scaleSet string, wantedCapacity int64) capacityPoller
|
||||
pollerOptions *poller.PollUntilDoneOptions
|
||||
}
|
||||
|
||||
// NewFromDefault creates a client with initialized clients.
|
||||
func NewFromDefault(subscriptionID, tenantID string) (*Client, error) {
|
||||
cred, err := azidentity.NewDefaultAzureCredential(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scaleSetAPI, err := armcomputev2.NewVirtualMachineScaleSetsClient(subscriptionID, cred, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
virtualMachineScaleSetVMsAPI, err := armcomputev2.NewVirtualMachineScaleSetVMsClient(subscriptionID, cred, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Client{
|
||||
scaleSetsAPI: scaleSetAPI,
|
||||
virtualMachineScaleSetVMsAPI: virtualMachineScaleSetVMsAPI,
|
||||
capacityPollerGenerator: func(resourceGroup, scaleSet string, wantedCapacity int64) capacityPoller {
|
||||
return poller.New[int64](&capacityPollingHandler{
|
||||
resourceGroup: resourceGroup,
|
||||
scaleSet: scaleSet,
|
||||
wantedCapacity: wantedCapacity,
|
||||
scaleSetsAPI: scaleSetAPI,
|
||||
})
|
||||
},
|
||||
}, nil
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
|
||||
armcomputev2 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
|
||||
)
|
||||
|
||||
type stubScaleSetsAPI struct {
|
||||
scaleSet armcomputev2.VirtualMachineScaleSetsClientGetResponse
|
||||
getErr error
|
||||
updateResponse armcomputev2.VirtualMachineScaleSetsClientUpdateResponse
|
||||
updateErr error
|
||||
deleteResponse armcomputev2.VirtualMachineScaleSetsClientDeleteInstancesResponse
|
||||
deleteErr error
|
||||
resultErr error
|
||||
}
|
||||
|
||||
func (a *stubScaleSetsAPI) Get(ctx context.Context, resourceGroupName string, vmScaleSetName string,
|
||||
options *armcomputev2.VirtualMachineScaleSetsClientGetOptions,
|
||||
) (armcomputev2.VirtualMachineScaleSetsClientGetResponse, error) {
|
||||
return a.scaleSet, a.getErr
|
||||
}
|
||||
|
||||
func (a *stubScaleSetsAPI) BeginUpdate(ctx context.Context, resourceGroupName string, vmScaleSetName string, parameters armcomputev2.VirtualMachineScaleSetUpdate,
|
||||
options *armcomputev2.VirtualMachineScaleSetsClientBeginUpdateOptions,
|
||||
) (*runtime.Poller[armcomputev2.VirtualMachineScaleSetsClientUpdateResponse], error) {
|
||||
poller, err := runtime.NewPoller(nil, runtime.NewPipeline("", "", runtime.PipelineOptions{}, nil), &runtime.NewPollerOptions[armcomputev2.VirtualMachineScaleSetsClientUpdateResponse]{
|
||||
Handler: &stubPoller[armcomputev2.VirtualMachineScaleSetsClientUpdateResponse]{
|
||||
result: a.updateResponse,
|
||||
resultErr: a.resultErr,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return poller, a.updateErr
|
||||
}
|
||||
|
||||
func (a *stubScaleSetsAPI) BeginDeleteInstances(ctx context.Context, resourceGroupName string, vmScaleSetName string, vmInstanceIDs armcomputev2.VirtualMachineScaleSetVMInstanceRequiredIDs,
|
||||
options *armcomputev2.VirtualMachineScaleSetsClientBeginDeleteInstancesOptions,
|
||||
) (*runtime.Poller[armcomputev2.VirtualMachineScaleSetsClientDeleteInstancesResponse], error) {
|
||||
poller, err := runtime.NewPoller(nil, runtime.NewPipeline("", "", runtime.PipelineOptions{}, nil), &runtime.NewPollerOptions[armcomputev2.VirtualMachineScaleSetsClientDeleteInstancesResponse]{
|
||||
Handler: &stubPoller[armcomputev2.VirtualMachineScaleSetsClientDeleteInstancesResponse]{
|
||||
result: a.deleteResponse,
|
||||
resultErr: a.resultErr,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return poller, a.deleteErr
|
||||
}
|
||||
|
||||
type stubvirtualMachineScaleSetVMsAPI struct {
|
||||
scaleSetVM armcomputev2.VirtualMachineScaleSetVMsClientGetResponse
|
||||
getErr error
|
||||
instanceView armcomputev2.VirtualMachineScaleSetVMsClientGetInstanceViewResponse
|
||||
instanceViewErr error
|
||||
pager *stubPager
|
||||
}
|
||||
|
||||
func (a *stubvirtualMachineScaleSetVMsAPI) Get(ctx context.Context, resourceGroupName string, vmScaleSetName string, instanceID string,
|
||||
options *armcomputev2.VirtualMachineScaleSetVMsClientGetOptions,
|
||||
) (armcomputev2.VirtualMachineScaleSetVMsClientGetResponse, error) {
|
||||
return a.scaleSetVM, a.getErr
|
||||
}
|
||||
|
||||
func (a *stubvirtualMachineScaleSetVMsAPI) GetInstanceView(ctx context.Context, resourceGroupName string, vmScaleSetName string, instanceID string,
|
||||
options *armcomputev2.VirtualMachineScaleSetVMsClientGetInstanceViewOptions,
|
||||
) (armcomputev2.VirtualMachineScaleSetVMsClientGetInstanceViewResponse, error) {
|
||||
return a.instanceView, a.instanceViewErr
|
||||
}
|
||||
|
||||
func (a *stubvirtualMachineScaleSetVMsAPI) NewListPager(resourceGroupName string, virtualMachineScaleSetName string,
|
||||
options *armcomputev2.VirtualMachineScaleSetVMsClientListOptions,
|
||||
) *runtime.Pager[armcomputev2.VirtualMachineScaleSetVMsClientListResponse] {
|
||||
return runtime.NewPager(runtime.PagingHandler[armcomputev2.VirtualMachineScaleSetVMsClientListResponse]{
|
||||
More: a.pager.moreFunc(),
|
||||
Fetcher: a.pager.fetcherFunc(),
|
||||
})
|
||||
}
|
||||
|
||||
type stubPoller[T any] struct {
|
||||
result T
|
||||
pollErr error
|
||||
resultErr error
|
||||
}
|
||||
|
||||
func (p *stubPoller[T]) Done() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *stubPoller[T]) Poll(context.Context) (*http.Response, error) {
|
||||
return nil, p.pollErr
|
||||
}
|
||||
|
||||
func (p *stubPoller[T]) Result(ctx context.Context, out *T) error {
|
||||
*out = p.result
|
||||
return p.resultErr
|
||||
}
|
||||
|
||||
type stubPager struct {
|
||||
list []armcomputev2.VirtualMachineScaleSetVM
|
||||
fetchErr error
|
||||
more bool
|
||||
}
|
||||
|
||||
func (p *stubPager) moreFunc() func(armcomputev2.VirtualMachineScaleSetVMsClientListResponse) bool {
|
||||
return func(armcomputev2.VirtualMachineScaleSetVMsClientListResponse) bool {
|
||||
return p.more
|
||||
}
|
||||
}
|
||||
|
||||
func (p *stubPager) fetcherFunc() func(context.Context, *armcomputev2.VirtualMachineScaleSetVMsClientListResponse) (armcomputev2.VirtualMachineScaleSetVMsClientListResponse, error) {
|
||||
return func(context.Context, *armcomputev2.VirtualMachineScaleSetVMsClientListResponse) (armcomputev2.VirtualMachineScaleSetVMsClientListResponse, error) {
|
||||
page := make([]*armcomputev2.VirtualMachineScaleSetVM, len(p.list))
|
||||
for i := range p.list {
|
||||
page[i] = &p.list[i]
|
||||
}
|
||||
return armcomputev2.VirtualMachineScaleSetVMsClientListResponse{
|
||||
VirtualMachineScaleSetVMListResult: armcomputev2.VirtualMachineScaleSetVMListResult{
|
||||
Value: page,
|
||||
},
|
||||
}, p.fetchErr
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
armcomputev2 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
|
||||
updatev1alpha1 "github.com/edgelesssys/constellation/operators/constellation-node-operator/api/v1alpha1"
|
||||
)
|
||||
|
||||
const (
|
||||
provisioningStateCreating = "ProvisioningState/creating"
|
||||
provisioningStateUpdating = "ProvisioningState/updating"
|
||||
provisioningStateMigrating = "ProvisioningState/migrating"
|
||||
provisioningStateFailed = "ProvisioningState/failed"
|
||||
provisioningStateDeleting = "ProvisioningState/deleting"
|
||||
powerStateStarting = "PowerState/starting"
|
||||
powerStateStopping = "PowerState/stopping"
|
||||
powerStateStopped = "PowerState/stopped"
|
||||
powerStateDeallocating = "PowerState/deallocating"
|
||||
powerStateDeallocated = "PowerState/deallocated"
|
||||
powerStateRunning = "PowerState/running"
|
||||
)
|
||||
|
||||
// nodeStateFromStatuses returns the node state from instance view statuses.
|
||||
// reference:
|
||||
// - https://docs.microsoft.com/en-us/azure/virtual-machines/states-billing#provisioning-states
|
||||
// - https://docs.microsoft.com/en-us/azure/virtual-machines/states-billing#power-states-and-billing
|
||||
func nodeStateFromStatuses(statuses []*armcomputev2.InstanceViewStatus) updatev1alpha1.CSPNodeState {
|
||||
for _, status := range statuses {
|
||||
if status == nil || status.Code == nil {
|
||||
continue
|
||||
}
|
||||
switch *status.Code {
|
||||
case provisioningStateCreating:
|
||||
return updatev1alpha1.NodeStateCreating
|
||||
case provisioningStateUpdating:
|
||||
return updatev1alpha1.NodeStateStopped
|
||||
case provisioningStateMigrating:
|
||||
return updatev1alpha1.NodeStateStopped
|
||||
case provisioningStateFailed:
|
||||
return updatev1alpha1.NodeStateFailed
|
||||
case provisioningStateDeleting:
|
||||
return updatev1alpha1.NodeStateTerminating
|
||||
case powerStateStarting:
|
||||
return updatev1alpha1.NodeStateCreating
|
||||
case powerStateStopping:
|
||||
return updatev1alpha1.NodeStateStopped
|
||||
case powerStateStopped:
|
||||
return updatev1alpha1.NodeStateStopped
|
||||
case powerStateDeallocating:
|
||||
return updatev1alpha1.NodeStateStopped
|
||||
case powerStateDeallocated:
|
||||
return updatev1alpha1.NodeStateStopped
|
||||
case powerStateRunning:
|
||||
return updatev1alpha1.NodeStateReady
|
||||
}
|
||||
}
|
||||
return updatev1alpha1.NodeStateUnknown
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
|
||||
armcomputev2 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
updatev1alpha1 "github.com/edgelesssys/constellation/operators/constellation-node-operator/api/v1alpha1"
|
||||
)
|
||||
|
||||
// this state is included in most VMs but not needed
|
||||
// to determine the node state as every provisioned VM also has a power state
|
||||
const provisioningStateSucceeded = "ProvisioningState/succeeded"
|
||||
|
||||
func TestNodeStateFromStatuses(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
statuses []*armcomputev2.InstanceViewStatus
|
||||
wantState updatev1alpha1.CSPNodeState
|
||||
}{
|
||||
"no statuses": {
|
||||
wantState: updatev1alpha1.NodeStateUnknown,
|
||||
},
|
||||
"invalid status": {
|
||||
statuses: []*armcomputev2.InstanceViewStatus{nil, {Code: nil}},
|
||||
wantState: updatev1alpha1.NodeStateUnknown,
|
||||
},
|
||||
"provisioning creating": {
|
||||
statuses: []*armcomputev2.InstanceViewStatus{{Code: to.Ptr(provisioningStateCreating)}},
|
||||
wantState: updatev1alpha1.NodeStateCreating,
|
||||
},
|
||||
"provisioning updating": {
|
||||
statuses: []*armcomputev2.InstanceViewStatus{{Code: to.Ptr(provisioningStateUpdating)}},
|
||||
wantState: updatev1alpha1.NodeStateStopped,
|
||||
},
|
||||
"provisioning migrating": {
|
||||
statuses: []*armcomputev2.InstanceViewStatus{{Code: to.Ptr(provisioningStateMigrating)}},
|
||||
wantState: updatev1alpha1.NodeStateStopped,
|
||||
},
|
||||
"provisioning failed": {
|
||||
statuses: []*armcomputev2.InstanceViewStatus{{Code: to.Ptr(provisioningStateFailed)}},
|
||||
wantState: updatev1alpha1.NodeStateFailed,
|
||||
},
|
||||
"provisioning deleting": {
|
||||
statuses: []*armcomputev2.InstanceViewStatus{{Code: to.Ptr(provisioningStateDeleting)}},
|
||||
wantState: updatev1alpha1.NodeStateTerminating,
|
||||
},
|
||||
"provisioning succeeded (but no power state)": {
|
||||
statuses: []*armcomputev2.InstanceViewStatus{{Code: to.Ptr(provisioningStateSucceeded)}},
|
||||
wantState: updatev1alpha1.NodeStateUnknown,
|
||||
},
|
||||
"power starting": {
|
||||
statuses: []*armcomputev2.InstanceViewStatus{
|
||||
{Code: to.Ptr(provisioningStateSucceeded)},
|
||||
{Code: to.Ptr(powerStateStarting)},
|
||||
},
|
||||
wantState: updatev1alpha1.NodeStateCreating,
|
||||
},
|
||||
"power stopping": {
|
||||
statuses: []*armcomputev2.InstanceViewStatus{
|
||||
{Code: to.Ptr(provisioningStateSucceeded)},
|
||||
{Code: to.Ptr(powerStateStopping)},
|
||||
},
|
||||
wantState: updatev1alpha1.NodeStateStopped,
|
||||
},
|
||||
"power stopped": {
|
||||
statuses: []*armcomputev2.InstanceViewStatus{
|
||||
{Code: to.Ptr(provisioningStateSucceeded)},
|
||||
{Code: to.Ptr(powerStateStopped)},
|
||||
},
|
||||
wantState: updatev1alpha1.NodeStateStopped,
|
||||
},
|
||||
"power deallocating": {
|
||||
statuses: []*armcomputev2.InstanceViewStatus{
|
||||
{Code: to.Ptr(provisioningStateSucceeded)},
|
||||
{Code: to.Ptr(powerStateDeallocating)},
|
||||
},
|
||||
wantState: updatev1alpha1.NodeStateStopped,
|
||||
},
|
||||
"power deallocated": {
|
||||
statuses: []*armcomputev2.InstanceViewStatus{
|
||||
{Code: to.Ptr(provisioningStateSucceeded)},
|
||||
{Code: to.Ptr(powerStateDeallocated)},
|
||||
},
|
||||
wantState: updatev1alpha1.NodeStateStopped,
|
||||
},
|
||||
"power running": {
|
||||
statuses: []*armcomputev2.InstanceViewStatus{
|
||||
{Code: to.Ptr(provisioningStateSucceeded)},
|
||||
{Code: to.Ptr(powerStateRunning)},
|
||||
},
|
||||
wantState: updatev1alpha1.NodeStateReady,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
gotState := nodeStateFromStatuses(tc.statuses)
|
||||
assert.Equal(tc.wantState, gotState)
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,156 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
|
||||
)
|
||||
|
||||
// GetNodeImage returns the image name of the node.
|
||||
func (c *Client) GetNodeImage(ctx context.Context, providerID string) (string, error) {
|
||||
_, resourceGroup, scaleSet, instanceID, err := scaleSetInformationFromProviderID(providerID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
resp, err := c.virtualMachineScaleSetVMsAPI.Get(ctx, resourceGroup, scaleSet, instanceID, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if resp.Properties == nil ||
|
||||
resp.Properties.StorageProfile == nil ||
|
||||
resp.Properties.StorageProfile.ImageReference == nil ||
|
||||
resp.Properties.StorageProfile.ImageReference.ID == nil {
|
||||
return "", fmt.Errorf("node %q does not have valid image reference", providerID)
|
||||
}
|
||||
return *resp.Properties.StorageProfile.ImageReference.ID, nil
|
||||
}
|
||||
|
||||
// GetScalingGroupID returns the scaling group ID of the node.
|
||||
func (c *Client) GetScalingGroupID(ctx context.Context, providerID string) (string, error) {
|
||||
subscriptionID, resourceGroup, scaleSet, _, err := scaleSetInformationFromProviderID(providerID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return joinVMSSID(subscriptionID, resourceGroup, scaleSet), nil
|
||||
}
|
||||
|
||||
// CreateNode creates a node in the specified scaling group.
|
||||
func (c *Client) CreateNode(ctx context.Context, scalingGroupID string) (nodeName, providerID string, err error) {
|
||||
_, resourceGroup, scaleSet, err := splitVMSSID(scalingGroupID)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// get list of instance IDs before scaling,
|
||||
var oldVMIDs []string
|
||||
pager := c.virtualMachineScaleSetVMsAPI.NewListPager(resourceGroup, scaleSet, nil)
|
||||
for pager.More() {
|
||||
page, err := pager.NextPage(ctx)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
for _, vm := range page.Value {
|
||||
if vm == nil || vm.ID == nil {
|
||||
continue
|
||||
}
|
||||
oldVMIDs = append(oldVMIDs, *vm.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// increase the number of instances by one
|
||||
resp, err := c.scaleSetsAPI.Get(ctx, resourceGroup, scaleSet, nil)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if resp.SKU == nil || resp.SKU.Capacity == nil {
|
||||
return "", "", fmt.Errorf("scale set %q does not have valid capacity", scaleSet)
|
||||
}
|
||||
wantedCapacity := *resp.SKU.Capacity + 1
|
||||
_, err = c.scaleSetsAPI.BeginUpdate(ctx, resourceGroup, scaleSet, armcompute.VirtualMachineScaleSetUpdate{
|
||||
SKU: &armcompute.SKU{
|
||||
Capacity: &wantedCapacity,
|
||||
},
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
poller := c.capacityPollerGenerator(resourceGroup, scaleSet, wantedCapacity)
|
||||
if _, err := poller.PollUntilDone(ctx, c.pollerOptions); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// get the list of instances again
|
||||
// and find the new instance id by comparing the old and new lists
|
||||
pager = c.virtualMachineScaleSetVMsAPI.NewListPager(resourceGroup, scaleSet, nil)
|
||||
for pager.More() {
|
||||
page, err := pager.NextPage(ctx)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
for _, vm := range page.Value {
|
||||
// check if the instance already existed in the old list
|
||||
if !hasKnownVMID(vm, oldVMIDs) {
|
||||
return strings.ToLower(*vm.Properties.OSProfile.ComputerName), "azure://" + *vm.ID, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", "", fmt.Errorf("failed to find new node after scaling up")
|
||||
}
|
||||
|
||||
// DeleteNode deletes a node specified by its provider ID.
|
||||
func (c *Client) DeleteNode(ctx context.Context, providerID string) error {
|
||||
_, resourceGroup, scaleSet, instanceID, err := scaleSetInformationFromProviderID(providerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ids := armcompute.VirtualMachineScaleSetVMInstanceRequiredIDs{InstanceIDs: []*string{&instanceID}}
|
||||
_, err = c.scaleSetsAPI.BeginDeleteInstances(ctx, resourceGroup, scaleSet, ids, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// capacityPollingHandler polls a scale set
|
||||
// until its capacity reaches the desired value.
|
||||
type capacityPollingHandler struct {
|
||||
done bool
|
||||
wantedCapacity int64
|
||||
resourceGroup string
|
||||
scaleSet string
|
||||
scaleSetsAPI
|
||||
}
|
||||
|
||||
func (h *capacityPollingHandler) Done() bool {
|
||||
return h.done
|
||||
}
|
||||
|
||||
func (h *capacityPollingHandler) Poll(ctx context.Context) error {
|
||||
resp, err := h.scaleSetsAPI.Get(ctx, h.resourceGroup, h.scaleSet, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.SKU == nil || resp.SKU.Capacity == nil {
|
||||
return fmt.Errorf("scale set %q does not have valid capacity", h.scaleSet)
|
||||
}
|
||||
h.done = *resp.SKU.Capacity == h.wantedCapacity
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *capacityPollingHandler) Result(ctx context.Context, out *int64) error {
|
||||
if !h.done {
|
||||
return fmt.Errorf("failed to scale up")
|
||||
}
|
||||
*out = h.wantedCapacity
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasKnownVMID returns true if the vmID is found in the vm ID list.
|
||||
func hasKnownVMID(vm *armcompute.VirtualMachineScaleSetVM, vmIDs []string) bool {
|
||||
for _, id := range vmIDs {
|
||||
if vm != nil && vm.ID != nil && *vm.ID == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
@ -0,0 +1,353 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
|
||||
armcomputev2 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
|
||||
"github.com/edgelesssys/constellation/operators/constellation-node-operator/internal/poller"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetNodeImage(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
providerID string
|
||||
vm armcomputev2.VirtualMachineScaleSetVM
|
||||
getScaleSetVMErr error
|
||||
wantImage string
|
||||
wantErr bool
|
||||
}{
|
||||
"getting node image works": {
|
||||
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
|
||||
vm: armcomputev2.VirtualMachineScaleSetVM{
|
||||
Properties: &armcomputev2.VirtualMachineScaleSetVMProperties{
|
||||
StorageProfile: &armcomputev2.StorageProfile{
|
||||
ImageReference: &armcomputev2.ImageReference{
|
||||
ID: to.Ptr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/images/image-name"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantImage: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/images/image-name",
|
||||
},
|
||||
"splitting providerID fails": {
|
||||
providerID: "invalid",
|
||||
wantErr: true,
|
||||
},
|
||||
"get scale set vm fails": {
|
||||
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
|
||||
getScaleSetVMErr: errors.New("get scale set vm error"),
|
||||
wantErr: true,
|
||||
},
|
||||
"scale set vm does not have valid image reference": {
|
||||
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
|
||||
vm: armcomputev2.VirtualMachineScaleSetVM{},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
client := Client{
|
||||
virtualMachineScaleSetVMsAPI: &stubvirtualMachineScaleSetVMsAPI{
|
||||
scaleSetVM: armcomputev2.VirtualMachineScaleSetVMsClientGetResponse{
|
||||
VirtualMachineScaleSetVM: tc.vm,
|
||||
},
|
||||
getErr: tc.getScaleSetVMErr,
|
||||
},
|
||||
}
|
||||
gotImage, err := client.GetNodeImage(context.Background(), tc.providerID)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.wantImage, gotImage)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetScalingGroupID(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
providerID string
|
||||
wantScalingGroupID string
|
||||
wantErr bool
|
||||
}{
|
||||
"getting scaling group id works": {
|
||||
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
|
||||
wantScalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
|
||||
},
|
||||
"splitting providerID fails": {
|
||||
providerID: "invalid",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
client := Client{}
|
||||
gotScalingGroupID, err := client.GetScalingGroupID(context.Background(), tc.providerID)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.wantScalingGroupID, gotScalingGroupID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateNode(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
scalingGroupID string
|
||||
sku *armcomputev2.SKU
|
||||
preexistingVMs []armcomputev2.VirtualMachineScaleSetVM
|
||||
newVM *armcomputev2.VirtualMachineScaleSetVM
|
||||
fetchErr error
|
||||
pollErr error
|
||||
getSKUCapacityErr error
|
||||
updateScaleSetErr error
|
||||
wantNodeName string
|
||||
wantProviderID string
|
||||
wantEarlyErr bool
|
||||
wantLateErr bool
|
||||
}{
|
||||
"creating node works": {
|
||||
scalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
|
||||
sku: &armcomputev2.SKU{
|
||||
Capacity: to.Ptr(int64(0)),
|
||||
},
|
||||
newVM: &armcomputev2.VirtualMachineScaleSetVM{
|
||||
ID: to.Ptr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/node-name"),
|
||||
Properties: &armcomputev2.VirtualMachineScaleSetVMProperties{
|
||||
OSProfile: &armcomputev2.OSProfile{
|
||||
ComputerName: to.Ptr("node-name"),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantNodeName: "node-name",
|
||||
wantProviderID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/node-name",
|
||||
},
|
||||
"creating node works with existing nodes": {
|
||||
scalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
|
||||
sku: &armcomputev2.SKU{
|
||||
Capacity: to.Ptr(int64(1)),
|
||||
},
|
||||
preexistingVMs: []armcomputev2.VirtualMachineScaleSetVM{
|
||||
{ID: to.Ptr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/preexisting-node")},
|
||||
},
|
||||
newVM: &armcomputev2.VirtualMachineScaleSetVM{
|
||||
ID: to.Ptr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/new-node"),
|
||||
Properties: &armcomputev2.VirtualMachineScaleSetVMProperties{
|
||||
OSProfile: &armcomputev2.OSProfile{
|
||||
ComputerName: to.Ptr("new-node"),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantNodeName: "new-node",
|
||||
wantProviderID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/new-node",
|
||||
},
|
||||
"splitting scalingGroupID fails": {
|
||||
scalingGroupID: "invalid",
|
||||
wantEarlyErr: true,
|
||||
},
|
||||
"getting node list fails": {
|
||||
scalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
|
||||
fetchErr: errors.New("get node list error"),
|
||||
wantEarlyErr: true,
|
||||
},
|
||||
"getting SKU capacity fails": {
|
||||
scalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
|
||||
getSKUCapacityErr: errors.New("get sku capacity error"),
|
||||
wantEarlyErr: true,
|
||||
},
|
||||
"sku is invalid": {
|
||||
scalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
|
||||
wantEarlyErr: true,
|
||||
},
|
||||
"updating scale set capacity fails": {
|
||||
scalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
|
||||
sku: &armcomputev2.SKU{Capacity: to.Ptr(int64(0))},
|
||||
updateScaleSetErr: errors.New("update scale set error"),
|
||||
wantEarlyErr: true,
|
||||
},
|
||||
"polling for increased capacity fails": {
|
||||
scalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
|
||||
sku: &armcomputev2.SKU{Capacity: to.Ptr(int64(0))},
|
||||
pollErr: errors.New("poll error"),
|
||||
wantLateErr: true,
|
||||
},
|
||||
"new node cannot be found": {
|
||||
scalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
|
||||
sku: &armcomputev2.SKU{Capacity: to.Ptr(int64(0))},
|
||||
wantLateErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
pager := &stubPager{
|
||||
list: tc.preexistingVMs,
|
||||
fetchErr: tc.fetchErr,
|
||||
}
|
||||
poller := NewStubCapacityPoller(tc.pollErr)
|
||||
client := Client{
|
||||
virtualMachineScaleSetVMsAPI: &stubvirtualMachineScaleSetVMsAPI{
|
||||
pager: pager,
|
||||
},
|
||||
scaleSetsAPI: &stubScaleSetsAPI{
|
||||
scaleSet: armcomputev2.VirtualMachineScaleSetsClientGetResponse{
|
||||
VirtualMachineScaleSet: armcomputev2.VirtualMachineScaleSet{
|
||||
SKU: tc.sku,
|
||||
},
|
||||
},
|
||||
getErr: tc.getSKUCapacityErr,
|
||||
updateErr: tc.updateScaleSetErr,
|
||||
},
|
||||
capacityPollerGenerator: func(resourceGroup, scaleSet string, wantedCapacity int64) capacityPoller {
|
||||
return poller
|
||||
},
|
||||
}
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
var gotNodeName, gotProviderID string
|
||||
var createErr error
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
gotNodeName, gotProviderID, createErr = client.CreateNode(context.Background(), tc.scalingGroupID)
|
||||
}()
|
||||
|
||||
// want error before PollUntilDone is called
|
||||
if tc.wantEarlyErr {
|
||||
wg.Wait()
|
||||
assert.Error(createErr)
|
||||
return
|
||||
}
|
||||
|
||||
// wait for PollUntilDone to be called
|
||||
<-poller.pollC
|
||||
// update list of nodes
|
||||
if tc.newVM != nil {
|
||||
pager.list = append(pager.list, *tc.newVM)
|
||||
}
|
||||
// let PollUntilDone finish
|
||||
poller.doneC <- struct{}{}
|
||||
|
||||
wg.Wait()
|
||||
if tc.wantLateErr {
|
||||
assert.Error(createErr)
|
||||
return
|
||||
}
|
||||
require.NoError(createErr)
|
||||
assert.Equal(tc.wantNodeName, gotNodeName)
|
||||
assert.Equal(tc.wantProviderID, gotProviderID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteNode(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
providerID string
|
||||
deleteErr error
|
||||
wantErr bool
|
||||
}{
|
||||
"deleting node works": {
|
||||
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
|
||||
},
|
||||
"invalid providerID": {
|
||||
providerID: "invalid",
|
||||
wantErr: true,
|
||||
},
|
||||
"deleting node fails": {
|
||||
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
|
||||
deleteErr: errors.New("delete error"),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
client := Client{
|
||||
scaleSetsAPI: &stubScaleSetsAPI{deleteErr: tc.deleteErr},
|
||||
}
|
||||
err := client.DeleteNode(context.Background(), tc.providerID)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
assert.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: test capacityPollingHandler
|
||||
|
||||
func TestCapacityPollingHandler(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
wantCapacity := int64(1)
|
||||
var gotCapacity int64
|
||||
handler := capacityPollingHandler{
|
||||
scaleSetsAPI: &stubScaleSetsAPI{
|
||||
scaleSet: armcomputev2.VirtualMachineScaleSetsClientGetResponse{
|
||||
VirtualMachineScaleSet: armcomputev2.VirtualMachineScaleSet{
|
||||
SKU: &armcomputev2.SKU{Capacity: to.Ptr(int64(0))},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantedCapacity: wantCapacity,
|
||||
}
|
||||
assert.NoError(handler.Poll(context.Background()))
|
||||
assert.False(handler.Done())
|
||||
// Calling Result early should error
|
||||
assert.Error(handler.Result(context.Background(), &gotCapacity))
|
||||
|
||||
// let scaleSet API error
|
||||
handler.scaleSetsAPI.(*stubScaleSetsAPI).getErr = errors.New("get error")
|
||||
assert.Error(handler.Poll(context.Background()))
|
||||
handler.scaleSetsAPI.(*stubScaleSetsAPI).getErr = nil
|
||||
|
||||
// let scaleSet API return invalid SKU
|
||||
handler.scaleSetsAPI.(*stubScaleSetsAPI).scaleSet.SKU = nil
|
||||
assert.Error(handler.Poll(context.Background()))
|
||||
|
||||
// let Poll finish
|
||||
handler.scaleSetsAPI.(*stubScaleSetsAPI).scaleSet.SKU = &armcomputev2.SKU{Capacity: to.Ptr(int64(wantCapacity))}
|
||||
assert.NoError(handler.Poll(context.Background()))
|
||||
assert.True(handler.Done())
|
||||
assert.NoError(handler.Result(context.Background(), &gotCapacity))
|
||||
assert.Equal(wantCapacity, gotCapacity)
|
||||
}
|
||||
|
||||
type stubCapacityPoller struct {
|
||||
pollErr error
|
||||
pollC chan struct{}
|
||||
doneC chan struct{}
|
||||
}
|
||||
|
||||
func NewStubCapacityPoller(pollErr error) *stubCapacityPoller {
|
||||
return &stubCapacityPoller{
|
||||
pollErr: pollErr,
|
||||
pollC: make(chan struct{}),
|
||||
doneC: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *stubCapacityPoller) PollUntilDone(context.Context, *poller.PollUntilDoneOptions) (int64, error) {
|
||||
p.pollC <- struct{}{}
|
||||
<-p.doneC
|
||||
return 0, p.pollErr
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
updatev1alpha1 "github.com/edgelesssys/constellation/operators/constellation-node-operator/api/v1alpha1"
|
||||
)
|
||||
|
||||
// GetNodeState returns the state of the node.
|
||||
func (c *Client) GetNodeState(ctx context.Context, providerID string) (updatev1alpha1.CSPNodeState, error) {
|
||||
_, resourceGroup, scaleSet, instanceID, err := scaleSetInformationFromProviderID(providerID)
|
||||
if err != nil {
|
||||
return updatev1alpha1.NodeStateUnknown, err
|
||||
}
|
||||
instanceView, err := c.virtualMachineScaleSetVMsAPI.GetInstanceView(ctx, resourceGroup, scaleSet, instanceID, nil)
|
||||
if err != nil {
|
||||
var respErr *azcore.ResponseError
|
||||
if errors.As(err, &respErr) && respErr.StatusCode == http.StatusNotFound {
|
||||
return updatev1alpha1.NodeStateTerminated, nil
|
||||
}
|
||||
return updatev1alpha1.NodeStateUnknown, err
|
||||
}
|
||||
return nodeStateFromStatuses(instanceView.Statuses), nil
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
|
||||
armcomputev2 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
|
||||
updatev1alpha1 "github.com/edgelesssys/constellation/operators/constellation-node-operator/api/v1alpha1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetNodeState(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
providerID string
|
||||
instanceView armcomputev2.VirtualMachineScaleSetVMInstanceView
|
||||
getInstanceViewErr error
|
||||
wantState updatev1alpha1.CSPNodeState
|
||||
wantErr bool
|
||||
}{
|
||||
"getting node state works": {
|
||||
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
|
||||
instanceView: armcomputev2.VirtualMachineScaleSetVMInstanceView{
|
||||
Statuses: []*armcomputev2.InstanceViewStatus{
|
||||
{Code: to.Ptr("ProvisioningState/succeeded")},
|
||||
{Code: to.Ptr("PowerState/running")},
|
||||
},
|
||||
},
|
||||
wantState: updatev1alpha1.NodeStateReady,
|
||||
},
|
||||
"splitting providerID fails": {
|
||||
providerID: "invalid",
|
||||
wantErr: true,
|
||||
},
|
||||
"get instance view fails": {
|
||||
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
|
||||
getInstanceViewErr: errors.New("get instance view error"),
|
||||
wantErr: true,
|
||||
},
|
||||
"get instance view returns 404": {
|
||||
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
|
||||
getInstanceViewErr: &azcore.ResponseError{StatusCode: http.StatusNotFound},
|
||||
wantState: updatev1alpha1.NodeStateTerminated,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
client := Client{
|
||||
virtualMachineScaleSetVMsAPI: &stubvirtualMachineScaleSetVMsAPI{
|
||||
instanceView: armcomputev2.VirtualMachineScaleSetVMsClientGetInstanceViewResponse{
|
||||
VirtualMachineScaleSetVMInstanceView: tc.instanceView,
|
||||
},
|
||||
instanceViewErr: tc.getInstanceViewErr,
|
||||
},
|
||||
}
|
||||
gotState, err := client.GetNodeState(context.Background(), tc.providerID)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.wantState, gotState)
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// azureVMSSProviderIDRegexp describes the format of a kubernetes providerID for an azure virtual machine scale set (VMSS) VM.
|
||||
var azureVMSSProviderIDRegexp = regexp.MustCompile(`^azure:///subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/Microsoft.Compute/virtualMachineScaleSets/([^/]+)/virtualMachines/([^/]+)$`)
|
||||
|
||||
// scaleSetInformationFromProviderID splits a provider's id belonging to an azure scale set into core components.
|
||||
// A providerID for scale set VMs is build after the following schema:
|
||||
// - 'azure:///subscriptions/<subscription-id>/resourceGroups/<resource-group>/providers/Microsoft.Compute/virtualMachineScaleSets/<scale-set-name>/virtualMachines/<instance-id>'
|
||||
func scaleSetInformationFromProviderID(providerID string) (subscriptionID, resourceGroup, scaleSet, instanceID string, err error) {
|
||||
matches := azureVMSSProviderIDRegexp.FindStringSubmatch(providerID)
|
||||
if len(matches) != 5 {
|
||||
return "", "", "", "", errors.New("error splitting providerID")
|
||||
}
|
||||
return matches[1], matches[2], matches[3], matches[4], nil
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestScaleSetInformationFromProviderID(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
providerID string
|
||||
wantSubscriptionID string
|
||||
wantResourceGroup string
|
||||
wantScaleSet string
|
||||
wantInstanceID string
|
||||
wantErr bool
|
||||
}{
|
||||
"providerID for scale set instance works": {
|
||||
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
|
||||
wantSubscriptionID: "subscription-id",
|
||||
wantResourceGroup: "resource-group",
|
||||
wantScaleSet: "scale-set-name",
|
||||
wantInstanceID: "instance-id",
|
||||
},
|
||||
"providerID for individual instance must fail": {
|
||||
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachines/instance-name",
|
||||
wantErr: true,
|
||||
},
|
||||
"providerID is malformed": {
|
||||
providerID: "malformed-provider-id",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
subscriptionID, resourceGroup, scaleSet, instanceName, err := scaleSetInformationFromProviderID(tc.providerID)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
assert.NoError(err)
|
||||
assert.Equal(tc.wantSubscriptionID, subscriptionID)
|
||||
assert.Equal(tc.wantResourceGroup, resourceGroup)
|
||||
assert.Equal(tc.wantScaleSet, scaleSet)
|
||||
assert.Equal(tc.wantInstanceID, instanceName)
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
|
||||
)
|
||||
|
||||
// GetScalingGroupImage returns the image URI of the scaling group.
|
||||
func (c *Client) GetScalingGroupImage(ctx context.Context, scalingGroupID string) (string, error) {
|
||||
_, resourceGroup, scaleSet, err := splitVMSSID(scalingGroupID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res, err := c.scaleSetsAPI.Get(ctx, resourceGroup, scaleSet, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if res.Properties == nil ||
|
||||
res.Properties.VirtualMachineProfile == nil ||
|
||||
res.Properties.VirtualMachineProfile.StorageProfile == nil ||
|
||||
res.Properties.VirtualMachineProfile.StorageProfile.ImageReference == nil ||
|
||||
res.Properties.VirtualMachineProfile.StorageProfile.ImageReference.ID == nil {
|
||||
return "", fmt.Errorf("scalet set %q does not have valid image reference", scalingGroupID)
|
||||
}
|
||||
return *res.Properties.VirtualMachineProfile.StorageProfile.ImageReference.ID, nil
|
||||
}
|
||||
|
||||
// SetScalingGroupImage sets the image URI of the scaling group.
|
||||
func (c *Client) SetScalingGroupImage(ctx context.Context, scalingGroupID, imageURI string) error {
|
||||
_, resourceGroup, scaleSet, err := splitVMSSID(scalingGroupID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
poller, err := c.scaleSetsAPI.BeginUpdate(ctx, resourceGroup, scaleSet, armcompute.VirtualMachineScaleSetUpdate{
|
||||
Properties: &armcompute.VirtualMachineScaleSetUpdateProperties{
|
||||
VirtualMachineProfile: &armcompute.VirtualMachineScaleSetUpdateVMProfile{
|
||||
StorageProfile: &armcompute.VirtualMachineScaleSetUpdateStorageProfile{
|
||||
ImageReference: &armcompute.ImageReference{
|
||||
ID: &imageURI,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := poller.PollUntilDone(ctx, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
|
||||
armcomputev2 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetScalingGroupImage(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
scalingGroupID string
|
||||
scaleSet armcomputev2.VirtualMachineScaleSet
|
||||
getScaleSetErr error
|
||||
wantImage string
|
||||
wantErr bool
|
||||
}{
|
||||
"getting image works": {
|
||||
scalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
|
||||
scaleSet: armcomputev2.VirtualMachineScaleSet{
|
||||
Properties: &armcomputev2.VirtualMachineScaleSetProperties{
|
||||
VirtualMachineProfile: &armcomputev2.VirtualMachineScaleSetVMProfile{
|
||||
StorageProfile: &armcomputev2.VirtualMachineScaleSetStorageProfile{
|
||||
ImageReference: &armcomputev2.ImageReference{
|
||||
ID: to.Ptr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/images/image-name"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantImage: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/images/image-name",
|
||||
},
|
||||
"splitting scalingGroupID fails": {
|
||||
scalingGroupID: "invalid",
|
||||
wantErr: true,
|
||||
},
|
||||
"get scale set fails": {
|
||||
scalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
|
||||
getScaleSetErr: errors.New("get scale set error"),
|
||||
wantErr: true,
|
||||
},
|
||||
"scale set is invalid": {
|
||||
scalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
client := Client{
|
||||
scaleSetsAPI: &stubScaleSetsAPI{
|
||||
scaleSet: armcomputev2.VirtualMachineScaleSetsClientGetResponse{
|
||||
VirtualMachineScaleSet: tc.scaleSet,
|
||||
},
|
||||
getErr: tc.getScaleSetErr,
|
||||
},
|
||||
}
|
||||
gotImage, err := client.GetScalingGroupImage(context.Background(), tc.scalingGroupID)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.wantImage, gotImage)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetScalingGroupImage(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
scalingGroupID string
|
||||
imageURI string
|
||||
updateErr error
|
||||
resultErr error
|
||||
wantErr bool
|
||||
}{
|
||||
"setting image works": {
|
||||
scalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
|
||||
imageURI: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/images/image-name-2",
|
||||
},
|
||||
"splitting scalingGroupID fails": {
|
||||
scalingGroupID: "invalid",
|
||||
wantErr: true,
|
||||
},
|
||||
"beginning update fails": {
|
||||
scalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
|
||||
imageURI: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/images/image-name-2",
|
||||
updateErr: errors.New("update error"),
|
||||
wantErr: true,
|
||||
},
|
||||
"retrieving polling result fails": {
|
||||
scalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
|
||||
imageURI: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/images/image-name-2",
|
||||
resultErr: errors.New("result error"),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
client := Client{
|
||||
scaleSetsAPI: &stubScaleSetsAPI{
|
||||
updateErr: tc.updateErr,
|
||||
resultErr: tc.resultErr,
|
||||
},
|
||||
}
|
||||
err := client.SetScalingGroupImage(context.Background(), tc.scalingGroupID, tc.imageURI)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// vmssIDRegexp is describes the format of an azure virtual machine scale set (VMSS) id.
|
||||
var vmssIDRegexp = regexp.MustCompile(`^/subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/Microsoft.Compute/virtualMachineScaleSets/([^/]+)$`)
|
||||
|
||||
// joinVMSSID joins scale set parameters to generate a virtual machine scale set (VMSS) ID.
|
||||
// Format: /subscriptions/<subscription>/resourceGroups/<resource-group>/providers/Microsoft.Compute/virtualMachineScaleSets/<scale-set>
|
||||
func joinVMSSID(subscriptionID, resourceGroup, scaleSet string) string {
|
||||
return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/virtualMachineScaleSets/%s", subscriptionID, resourceGroup, scaleSet)
|
||||
}
|
||||
|
||||
// splitVMSSID splits a virtual machine scale set (VMSS) ID into its component.
|
||||
func splitVMSSID(vmssID string) (subscriptionID, resourceGroup, scaleSet string, err error) {
|
||||
matches := vmssIDRegexp.FindStringSubmatch(vmssID)
|
||||
if len(matches) != 4 {
|
||||
return "", "", "", fmt.Errorf("error splitting vmssID")
|
||||
}
|
||||
return matches[1], matches[2], matches[3], nil
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestJoinVMSSID(t *testing.T) {
|
||||
assert.Equal(t,
|
||||
"/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set",
|
||||
joinVMSSID("subscription-id", "resource-group", "scale-set"))
|
||||
}
|
||||
|
||||
func TestSplitVMSSID(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
vmssID string
|
||||
wantSubscriptionID string
|
||||
wantResourceGroup string
|
||||
wantScaleSet string
|
||||
wantInstanceID string
|
||||
wantErr bool
|
||||
}{
|
||||
"vmssID can be splitted": {
|
||||
vmssID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
|
||||
wantSubscriptionID: "subscription-id",
|
||||
wantResourceGroup: "resource-group",
|
||||
wantScaleSet: "scale-set-name",
|
||||
wantInstanceID: "instance-id",
|
||||
},
|
||||
"vmssID is malformed": {
|
||||
vmssID: "malformed-vmss-id",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
subscriptionID, resourceGroup, scaleSet, err := splitVMSSID(tc.vmssID)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
assert.NoError(err)
|
||||
assert.Equal(tc.wantSubscriptionID, subscriptionID)
|
||||
assert.Equal(tc.wantResourceGroup, resourceGroup)
|
||||
assert.Equal(tc.wantScaleSet, scaleSet)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user