Compare commits

..
Author SHA1 Message Date
Benjamin HolmesandJohan Brandhorst-Satzkorn 30ea98c877 Add support for Virtual Machine Scale Sets in the Boundary Azure Plugin (#22)
* first pass at pulling in VMSS data

* infer vmss set id and external name from vmss data as none of these items are present by default

* updated scale set name formatting to align with azure portal and json outputs

* Update plugin/plugin.go

Updated following code review

Co-authored-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>

* Update plugin/plugin.go

Updated following code review

Co-authored-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>

* complete refactoring of initial implementation following code review, taking into accounts comments and common sense updates

* updated README.md to reflect changes to support VMSS

* fixed typo in README.md

* WIP: initial updates following code review

* refactored resource functions

* removed instrumentation statements

* resolve ifname comments

* address formatting issues in accordance with style recommendations

---------

Co-authored-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>
2025-01-22 10:36:03 -08:00
Irena Rindos 84ee22dcfd chore: update deps (#25) 2025-01-07 12:50:22 -05:00
dependabot[bot] 755c21fa6e Bump golang.org/x/crypto from 0.21.0 to 0.31.0 (#24)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.21.0 to 0.31.0.
- [Commits](https://github.com/golang/crypto/compare/v0.21.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-11 16:21:51 -08:00
Johan Brandhorst-Satzkorn 122de4a8f3 Update CODEOWNERS (#23)
Make the owners effect with a wildcard
2024-11-18 16:23:24 -08:00
12 changed files with 1611 additions and 324 deletions

No files matched your search

+3
View File
@@ -16,3 +16,6 @@
# Test credentials and other items # Test credentials and other items
/plugin/private* /plugin/private*
# vscode settings
.vscode
+4 -6
View File
@@ -4,9 +4,9 @@ This repo contains a Host-type plugin for [HashiCorp
Boundary](https://www.boundaryproject.io/) allowing dynamically sourcing hosts Boundary](https://www.boundaryproject.io/) allowing dynamically sourcing hosts
from Azure. from Azure.
Host sets created with this plugin define filters which select and group like VM Host sets created with this plugin define filters which select and group Virtual Machines
resources within Azure; these host sets can in turn be added to targets within and Virtual Machine instances enscapsulated in both Flexible and Uniform Virtual Machine Scale Sets.
Boundary as host sources. These host sets can in turn be added to targets within Boundary as host sources.
At creation or update of a host catalog of this type, configuration of the At creation or update of a host catalog of this type, configuration of the
plugin is performed via the attribute/secret values passed to the create or plugin is performed via the attribute/secret values passed to the create or
@@ -15,8 +15,6 @@ are indicated below with the valid fields.
Current limitations of this plugin: Current limitations of this plugin:
- Only VMs can be added to host sets through this plugin, not any other type of
compute resource
- A client secret must be generated against a registered application with any - A client secret must be generated against a registered application with any
necessary permissions necessary permissions
- For now, credential rotation must be disabled using the `disable_credential_rotation` - For now, credential rotation must be disabled using the `disable_credential_rotation`
@@ -76,7 +74,7 @@ The following attributes are valid on an Azure host Set resource:
## Filters ## Filters
Azure Resource Manager is used to find matching VMs before their details are Azure Resource Manager is used to find matching VMs and VM Scale Sets before their details are
looked up. There are some limitations with the filtering syntax; specifically, looked up. There are some limitations with the filtering syntax; specifically,
when using tags, other types of filters (such as on resource type) are not when using tags, other types of filters (such as on resource type) are not
allowed. As a result, it will generally be useful to filter directly on tag allowed. As a result, it will generally be useful to filter directly on tag
+5 -4
View File
@@ -12,6 +12,7 @@ require (
github.com/manicminer/hamilton v0.46.0 github.com/manicminer/hamilton v0.46.0
github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/mapstructure v1.5.0
github.com/stretchr/testify v1.8.2 github.com/stretchr/testify v1.8.2
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
google.golang.org/grpc v1.56.3 google.golang.org/grpc v1.56.3
google.golang.org/protobuf v1.33.0 google.golang.org/protobuf v1.33.0
) )
@@ -48,11 +49,11 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.6.2 // indirect github.com/rogpeppe/go-internal v1.6.2 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect
golang.org/x/crypto v0.21.0 // indirect golang.org/x/crypto v0.31.0 // indirect
golang.org/x/net v0.23.0 // indirect golang.org/x/net v0.33.0 // indirect
golang.org/x/oauth2 v0.7.0 // indirect golang.org/x/oauth2 v0.7.0 // indirect
golang.org/x/sys v0.20.0 // indirect golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.21.0 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
+10 -9
View File
@@ -274,8 +274,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -337,8 +337,8 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 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-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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -355,6 +355,7 @@ 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-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-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-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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-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-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -386,8 +387,8 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -395,8 +396,8 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 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-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -442,7 +443,7 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/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-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20210101214203-2dba1e4ea05c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210101214203-2dba1e4ea05c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+50
View File
@@ -65,6 +65,56 @@ func getVirtualMachinesClient(opt ...Option) (*compute.VirtualMachinesClient, er
return &client, nil return &client, nil
} }
func getVirtualMachineScaleSetsClient(opt ...Option) (*compute.VirtualMachineScaleSetsClient, error) {
opts, err := getOpts(opt...)
if err != nil {
return nil, fmt.Errorf("error parsing ops: %w", err)
}
if opts.withSubscriptionId == "" {
return nil, errors.New("subscription id is empty")
}
if opts.withAuthorizer == nil {
return nil, errors.New("authorizer is nil")
}
var client compute.VirtualMachineScaleSetsClient
switch opts.withBaseUrl {
case "":
client = compute.NewVirtualMachineScaleSetsClient(opts.withSubscriptionId)
default:
client = compute.NewVirtualMachineScaleSetsClientWithBaseURI(opts.withBaseUrl, opts.withSubscriptionId)
}
client.Authorizer = opts.withAuthorizer
if err := client.AddToUserAgent(opts.withUserAgent); err != nil {
return nil, fmt.Errorf("error adding identifier to user-agent: %w", err)
}
return &client, nil
}
func getVirtualMachineScaleSetVMsClient(opt ...Option) (*compute.VirtualMachineScaleSetVMsClient, error) {
opts, err := getOpts(opt...)
if err != nil {
return nil, fmt.Errorf("error parsing ops: %w", err)
}
if opts.withSubscriptionId == "" {
return nil, errors.New("subscription id is empty")
}
if opts.withAuthorizer == nil {
return nil, errors.New("authorizer is nil")
}
var client compute.VirtualMachineScaleSetVMsClient
switch opts.withBaseUrl {
case "":
client = compute.NewVirtualMachineScaleSetVMsClient(opts.withSubscriptionId)
default:
client = compute.NewVirtualMachineScaleSetVMsClientWithBaseURI(opts.withBaseUrl, opts.withSubscriptionId)
}
client.Authorizer = opts.withAuthorizer
if err := client.AddToUserAgent(opts.withUserAgent); err != nil {
return nil, fmt.Errorf("error adding identifier to user-agent: %w", err)
}
return &client, nil
}
func getNetworkInterfacesClient(opt ...Option) (*network.InterfacesClient, error) { func getNetworkInterfacesClient(opt ...Option) (*network.InterfacesClient, error) {
opts, err := getOpts(opt...) opts, err := getOpts(opt...)
if err != nil { if err != nil {
+22 -14
View File
@@ -4,22 +4,30 @@
package plugin package plugin
const ( const (
constBaseUrl = "base_url" // Azure service constants
constMsComputeService = "Microsoft.Compute"
constMsNetworkService = "Microsoft.Network"
constSubscriptions = "subscriptions"
constResourceGroups = "resourceGroups"
constProviders = "providers"
// Resource type constants
constVirtualMachinesResource = "virtualMachines"
constVirtualMachineScaleSetsResource = "virtualMachineScaleSets"
constNetworkInterfacesResource = "networkInterfaces"
constPublicIpAddressesResource = "publicIPAddresses"
// Field constants
constFilter = "filter" constFilter = "filter"
constSubscriptionId = "subscription_id" constSecretId = "secret_id"
constClientId = "client_id"
constSecretValue = "secret_value" constSecretValue = "secret_value"
constCredsLastRotatedTime = "creds_last_rotated_time" constCredsLastRotatedTime = "creds_last_rotated_time"
constSecretId = "secret_id"
constTenantId = "tenant_id"
constDefaultFilter = "resourceType eq 'Microsoft.Compute/virtualMachines'"
constMsComputeService = "Microsoft.Compute"
constVirtualMachinesResource = "virtualMachines"
constMsNetworkService = "Microsoft.Network"
constNetworkInterfacesResource = "networkInterfaces"
constPublicIpAddressesResource = "publicIPAddresses"
constSubscriptions = "subscriptions"
constResourceGroups = "resourceGroups"
constProviders = "providers"
constDisableCredentialRotation = "disable_credential_rotation" constDisableCredentialRotation = "disable_credential_rotation"
constSubscriptionId = "subscription_id"
constTenantId = "tenant_id"
constClientId = "client_id"
constBaseUrl = "base_url"
// Worker pool configuration
constDefaultWorkers = 1000
) )
+277
View File
@@ -0,0 +1,277 @@
package plugin
import (
"context"
"fmt"
"strings"
"github.com/Azure/azure-sdk-for-go/profiles/latest/compute/mgmt/compute"
"github.com/Azure/azure-sdk-for-go/profiles/latest/network/mgmt/network"
)
type networkInfo struct {
IpAddresses []string
}
// Common interface for IP configuration processing
type networkProcessor interface {
processIPConfigurations(ctx context.Context, ipConfigs []network.InterfaceIPConfiguration, ifName string) (*networkInfo, error)
getPublicIPAddress(ctx context.Context, publicIPRef *network.PublicIPAddress, ipConf *network.InterfaceIPConfiguration, ifName string) (string, error)
getNetworkInterface(ctx context.Context, interfaceID string) (*network.Interface, string, error)
}
// Standard VM network processor
type vmNetworkProcessor struct {
clients *azureClients
resourceGroup string
}
// VMSS network processor
type vmssNetworkProcessor struct {
clients *azureClients
resourceGroup string
vmssName string
instanceID string
}
func processNetworkProfile(
ctx context.Context,
interfaces []compute.NetworkInterfaceReference,
processor networkProcessor,
targetInfo *networkInfo,
) error {
for _, ifaceRef := range interfaces {
if ifaceRef.ID == nil {
return fmt.Errorf("nil ID for network interface")
}
iface, ifName, err := processor.getNetworkInterface(ctx, *ifaceRef.ID)
if err != nil {
return err
}
if iface.InterfacePropertiesFormat == nil || iface.InterfacePropertiesFormat.IPConfigurations == nil {
continue
}
networkInfo, err := processor.processIPConfigurations(ctx, *iface.InterfacePropertiesFormat.IPConfigurations, ifName)
if err != nil {
return err
}
targetInfo.IpAddresses = append(targetInfo.IpAddresses, networkInfo.IpAddresses...)
}
return nil
}
func (p *vmNetworkProcessor) getNetworkInterface(ctx context.Context, interfaceID string) (*network.Interface, string, error) {
ifResGroup, ifName, err := splitId(interfaceID, constMsNetworkService, constNetworkInterfacesResource)
if err != nil {
return nil, "", fmt.Errorf("error splitting network interface id %q: %w", interfaceID, err)
}
iface, err := p.clients.ifClient.Get(ctx, ifResGroup, ifName, "")
if err != nil {
return nil, "", fmt.Errorf("error fetching network interface: %w", err)
}
return &iface, ifName, nil
}
func (p *vmNetworkProcessor) processIPConfigurations(
ctx context.Context,
ipConfigs []network.InterfaceIPConfiguration,
ifName string,
) (
*networkInfo,
error,
) {
tempInfo := &networkInfo{}
for _, ipconf := range ipConfigs {
// Process private IP
if ipconf.PrivateIPAddress != nil {
tempInfo.IpAddresses = append(tempInfo.IpAddresses, *ipconf.PrivateIPAddress)
}
// Process public IP if available
if ipconf.PublicIPAddress != nil {
ipAddress, err := p.getPublicIPAddress(ctx, ipconf.PublicIPAddress, &ipconf, ifName)
if err != nil {
return nil, err
}
tempInfo.IpAddresses = append(tempInfo.IpAddresses, ipAddress)
}
}
return tempInfo, nil
}
func (p *vmNetworkProcessor) getPublicIPAddress(
ctx context.Context,
publicIP *network.PublicIPAddress,
ipConf *network.InterfaceIPConfiguration,
ifName string,
) (
string,
error,
) {
if publicIP.ID == nil {
return "", fmt.Errorf("ip configuration has public IP address info but nil id")
}
ipResGroup, ipName, err := splitId(*publicIP.ID, constMsNetworkService, constPublicIpAddressesResource)
if err != nil {
return "", fmt.Errorf("error splitting public ip address id: %w", err)
}
pipInfo, err := p.clients.pipClient.Get(ctx, ipResGroup, ipName, "")
if err != nil {
return "", fmt.Errorf("error fetching public IP information: %w", err)
}
if pipInfo.PublicIPAddressPropertiesFormat == nil {
return "", fmt.Errorf("nil public ip address properties format for public ip %q", *publicIP.ID)
}
if pipInfo.PublicIPAddressPropertiesFormat.IPAddress == nil {
return "", fmt.Errorf("nil IP address for public ip %q", *publicIP.ID)
}
return *pipInfo.PublicIPAddressPropertiesFormat.IPAddress, nil
}
func (p *vmssNetworkProcessor) getNetworkInterface(ctx context.Context, interfaceID string) (*network.Interface, string, error) {
ifName := extractResourceSuffix(interfaceID)
iface, err := p.clients.ifClient.GetVirtualMachineScaleSetNetworkInterface(
ctx,
p.resourceGroup,
p.vmssName,
p.instanceID,
ifName,
"")
if err != nil {
return nil, "", fmt.Errorf("error fetching VMSS network interface: %w", err)
}
return &iface, ifName, nil
}
func (p *vmssNetworkProcessor) processIPConfigurations(
ctx context.Context,
ipConfigs []network.InterfaceIPConfiguration,
ifName string,
) (
*networkInfo,
error,
) {
tempInfo := &networkInfo{}
for _, ipconf := range ipConfigs {
if ipconf.PrivateIPAddress != nil {
tempInfo.IpAddresses = append(tempInfo.IpAddresses, *ipconf.PrivateIPAddress)
}
if ipconf.PublicIPAddress != nil {
ipAddress, err := p.getPublicIPAddress(ctx, ipconf.PublicIPAddress, &ipconf, ifName)
if err != nil {
return nil, err
}
tempInfo.IpAddresses = append(tempInfo.IpAddresses, ipAddress)
}
}
return tempInfo, nil
}
func (p *vmssNetworkProcessor) getPublicIPAddress(
ctx context.Context,
publicIP *network.PublicIPAddress,
ipConf *network.InterfaceIPConfiguration,
ifName string,
) (
string,
error,
) {
if publicIP == nil || publicIP.ID == nil || ipConf.Name == nil {
return "", fmt.Errorf("invalid public IP configuration")
}
pipName := extractResourceSuffix(*publicIP.ID)
pipInfo, err := p.clients.pipClient.GetVirtualMachineScaleSetPublicIPAddress(
ctx,
p.resourceGroup,
p.vmssName,
p.instanceID,
ifName,
*ipConf.Name,
pipName,
"")
if err != nil {
return "", fmt.Errorf("error fetching public IP information: %w", err)
}
if pipInfo.PublicIPAddressPropertiesFormat == nil {
return "", fmt.Errorf("nil public ip address properties format")
}
if pipInfo.PublicIPAddressPropertiesFormat.IPAddress == nil {
return "", fmt.Errorf("nil IP address for public ip %q", *publicIP.ID)
}
return *pipInfo.PublicIPAddressPropertiesFormat.IPAddress, nil
}
func processVMNetworkInterfaces(
ctx context.Context,
vm compute.VirtualMachine,
clients *azureClients,
resourceGroup string,
netInfo *networkInfo,
) error {
if vm.VirtualMachineProperties == nil || vm.VirtualMachineProperties.NetworkProfile == nil {
return fmt.Errorf("error fetching network profile")
}
processor := &vmNetworkProcessor{
clients: clients,
resourceGroup: resourceGroup,
}
return processNetworkProfile(ctx, *vm.VirtualMachineProperties.NetworkProfile.NetworkInterfaces, processor, netInfo)
}
func processVMSSNetworkInterfaces(
ctx context.Context,
vmssvm compute.VirtualMachineScaleSetVM,
resourceGroup, vmssName string,
clients *azureClients,
netInfo *networkInfo,
) error {
if vmssvm.VirtualMachineScaleSetVMProperties == nil ||
vmssvm.VirtualMachineScaleSetVMProperties.NetworkProfile == nil {
return fmt.Errorf("error fetching network profile")
}
if vmssvm.InstanceID == nil {
return fmt.Errorf("instance ID is nil")
}
processor := &vmssNetworkProcessor{
clients: clients,
resourceGroup: resourceGroup,
vmssName: vmssName,
instanceID: *vmssvm.InstanceID,
}
return processNetworkProfile(ctx,
*vmssvm.VirtualMachineScaleSetVMProperties.NetworkProfile.NetworkInterfaces, processor, netInfo)
}
// extractResourceSuffix extracts the resource name from the end of an Azure resource ID
func extractResourceSuffix(resourceID string) string {
parts := strings.Split(resourceID, "/")
if len(parts) > 0 {
return parts[len(parts)-1]
}
return ""
}
+366 -289
View File
@@ -1,6 +1,8 @@
// Copyright (c) HashiCorp, Inc. // Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
// Package plugin implements the Azure host plugin for Boundary,
// providing functionality to manage and list Azure hosts.
package plugin package plugin
import ( import (
@@ -8,17 +10,20 @@ import (
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
"sync"
"time" "time"
"github.com/Azure/azure-sdk-for-go/profiles/latest/compute/mgmt/compute"
"github.com/Azure/azure-sdk-for-go/profiles/latest/network/mgmt/network" "github.com/Azure/azure-sdk-for-go/profiles/latest/network/mgmt/network"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/resources" "github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/resources"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/structpb"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin" pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
) )
@@ -28,37 +33,19 @@ type AzurePlugin struct {
} }
// Ensure that we are implementing HostPluginServiceServer // Ensure that we are implementing HostPluginServiceServer
var ( var _ pb.HostPluginServiceServer = (*AzurePlugin)(nil)
_ pb.HostPluginServiceServer = (*AzurePlugin)(nil)
)
type SetAttributes struct { type SetAttributes struct {
Filter string `mapstructure:"filter"` Filter string `mapstructure:"filter"`
} }
func rotateCredFromCallback(ctx context.Context, catalog *hostcatalogs.HostCatalog) (*pb.HostCatalogPersisted, error) { type azureClients struct {
authzInfo, err := getAuthorizationInfo(catalog) resClient *resources.Client
if err != nil { vmClient *compute.VirtualMachinesClient
return nil, fmt.Errorf("error getting hauth config when creating catalog: %w", err) vmssClient *compute.VirtualMachineScaleSetsClient
} vmssvmClient *compute.VirtualMachineScaleSetVMsClient
newPass, err := rotateCredential(ctx, authzInfo) ifClient *network.InterfacesClient
if err != nil { pipClient *network.PublicIPAddressesClient
return nil, fmt.Errorf("error rotating credentials when creating catalog: %w", err)
}
if newPass == nil {
return nil, errors.New("new credential back from rotate is nil")
}
newSecrets, err := structpb.NewStruct(map[string]interface{}{
constSecretId: *newPass.KeyId,
constSecretValue: *newPass.SecretText,
constCredsLastRotatedTime: time.Now().Format(time.RFC3339Nano),
})
if err != nil {
return nil, fmt.Errorf("error formatting new credential data as struct")
}
return &pb.HostCatalogPersisted{
Secrets: newSecrets,
}, err
} }
func (p *AzurePlugin) OnCreateCatalog(_ context.Context, req *pb.OnCreateCatalogRequest) (*pb.OnCreateCatalogResponse, error) { func (p *AzurePlugin) OnCreateCatalog(_ context.Context, req *pb.OnCreateCatalogRequest) (*pb.OnCreateCatalogResponse, error) {
@@ -94,13 +81,10 @@ func (p *AzurePlugin) OnUpdateCatalog(_ context.Context, req *pb.OnUpdateCatalog
} }
currentCatalog := req.GetCurrentCatalog() currentCatalog := req.GetCurrentCatalog()
if currentCatalog == nil { if currentCatalog == nil {
// Should never happen
return nil, status.Error(codes.FailedPrecondition, "current catalog is nil") return nil, status.Error(codes.FailedPrecondition, "current catalog is nil")
} }
secrets := req.GetNewCatalog().GetSecrets() secrets := req.GetNewCatalog().GetSecrets()
if secrets == nil { if secrets == nil {
// If new secrets weren't passed in, don't rotate what we have on
// update.
return &pb.OnUpdateCatalogResponse{}, nil return &pb.OnUpdateCatalogResponse{}, nil
} }
if err := validateSecrets(secrets); err != nil { if err := validateSecrets(secrets); err != nil {
@@ -142,10 +126,161 @@ func (p *AzurePlugin) OnUpdateSet(_ context.Context, req *pb.OnUpdateSetRequest)
func (p *AzurePlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*pb.ListHostsResponse, error) { func (p *AzurePlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*pb.ListHostsResponse, error) {
if len(req.GetSets()) == 0 { if len(req.GetSets()) == 0 {
// Nothing to fetch
return &pb.ListHostsResponse{}, nil return &pb.ListHostsResponse{}, nil
} }
catalog := req.GetCatalog()
// Initialize Azure clients and authorization
clients, err := p.initializeAzureResources(req.GetCatalog(), req.GetPersisted().GetSecrets())
if err != nil {
return nil, err
}
// Find matching resources and map them to sets
vmResources, vmssResources, resourceToSetMap, err := p.findMatchingResources(ctx, req.GetSets(), clients.resClient)
if err != nil {
return nil, err
}
// Create mutex and errgroup for parallel processing
mu := &sync.Mutex{}
vmToNetworkMap := make(map[string]networkInfo)
g, ctx := errgroup.WithContext(ctx)
// Process standard VMs in parallel if they exist
if len(vmResources) > 0 {
g.Go(func() error {
vmNetworkMap, err := p.processStandardVMs(ctx, vmResources, clients)
if err != nil {
return fmt.Errorf("processing VMs: %w", err)
}
mu.Lock()
defer mu.Unlock()
for k, v := range vmNetworkMap {
vmToNetworkMap[k] = v
}
return nil
})
}
// Process VMSS in parallel if they exist
if len(vmssResources) > 0 {
g.Go(func() error {
vmssNetworkMap, err := p.processVMScaleSets(ctx, vmssResources, clients)
if err != nil {
return fmt.Errorf("processing VMSS: %w", err)
}
mu.Lock()
defer mu.Unlock()
for k, v := range vmssNetworkMap {
vmToNetworkMap[k] = v
}
return nil
})
}
// Wait for all goroutines to complete and check for errors
if err := g.Wait(); err != nil {
return nil, err
}
response := buildHostsResponse(vmToNetworkMap, resourceToSetMap)
return response, nil
}
func rotateCredFromCallback(ctx context.Context, catalog *hostcatalogs.HostCatalog) (*pb.HostCatalogPersisted, error) {
authzInfo, err := getAuthorizationInfo(catalog)
if err != nil {
return nil, fmt.Errorf("error getting auth config when creating catalog: %w", err)
}
newPass, err := rotateCredential(ctx, authzInfo)
if err != nil {
return nil, fmt.Errorf("error rotating credentials when creating catalog: %w", err)
}
if newPass == nil {
return nil, errors.New("new credential back from rotate is nil")
}
newSecrets, err := structpb.NewStruct(map[string]interface{}{
constSecretId: *newPass.KeyId,
constSecretValue: *newPass.SecretText,
constCredsLastRotatedTime: time.Now().Format(time.RFC3339Nano),
})
if err != nil {
return nil, fmt.Errorf("error formatting new credential data as struct")
}
return &pb.HostCatalogPersisted{
Secrets: newSecrets,
}, err
}
func buildHostsResponse(
vmToNetworkMap map[string]networkInfo,
resourceToSetMap map[string][]string) *pb.ListHostsResponse {
ret := &pb.ListHostsResponse{}
for resourceID, networkInfo := range vmToNetworkMap {
host, err := createHostFromResource(resourceID, networkInfo, resourceToSetMap)
if err != nil {
fmt.Printf("Error creating host from resource: %v\n", err)
continue
}
ret.Hosts = append(ret.Hosts, host)
}
return ret
}
func createHostFromResource(resourceID string, networkInfo networkInfo,
resourceToSetMap map[string][]string) (*pb.ListHostsResponseHost, error) {
// Extract the resource type from the ID
resourceType, err := extractResourceType(resourceID)
if err != nil {
return nil, fmt.Errorf("invalid resource ID: %w", err)
}
switch resourceType {
case constVirtualMachineScaleSetsResource:
externalName, err := getExternalNameforVMSSInstance(resourceID)
if err != nil {
externalName = "" // Use empty name if there's an error
}
setId, err := getSetForVMSSInstance(resourceID)
if err != nil {
return nil, fmt.Errorf("could not determine set for resource ID: %w", err)
}
return &pb.ListHostsResponseHost{
ExternalId: resourceID,
ExternalName: externalName,
IpAddresses: networkInfo.IpAddresses,
SetIds: resourceToSetMap[setId],
}, nil
case constVirtualMachinesResource:
_, externalName, err := splitId(resourceID, constMsComputeService, constVirtualMachinesResource)
if err != nil {
externalName = "" // Use empty name if there's an error
}
return &pb.ListHostsResponseHost{
ExternalId: resourceID,
ExternalName: externalName,
IpAddresses: networkInfo.IpAddresses,
SetIds: resourceToSetMap[resourceID],
}, nil
default:
return nil, fmt.Errorf("unsupported resource type: %s", resourceType) // Return explicit error
}
}
func (p *AzurePlugin) initializeAzureResources(
catalog *hostcatalogs.HostCatalog,
secrets *structpb.Struct) (*azureClients, error) {
if catalog == nil { if catalog == nil {
return nil, status.Error(codes.FailedPrecondition, "catalog is nil") return nil, status.Error(codes.FailedPrecondition, "catalog is nil")
} }
@@ -154,8 +289,8 @@ func (p *AzurePlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (
return nil, status.Error(codes.FailedPrecondition, "catalog has no attributes") return nil, status.Error(codes.FailedPrecondition, "catalog has no attributes")
} }
// Create an authorizer // Create authorizer
catalog.Secrets = req.GetPersisted().GetSecrets() catalog.Secrets = secrets
authzInfo, err := getAuthorizationInfo(catalog) authzInfo, err := getAuthorizationInfo(catalog)
if err != nil { if err != nil {
return nil, fmt.Errorf("error fetching authorizationInfo: %w", err) return nil, fmt.Errorf("error fetching authorizationInfo: %w", err)
@@ -163,6 +298,7 @@ func (p *AzurePlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (
if authzInfo == nil { if authzInfo == nil {
return nil, status.Error(codes.FailedPrecondition, "authorization info is nil") return nil, status.Error(codes.FailedPrecondition, "authorization info is nil")
} }
authorizer, err := authzInfo.autorestAuthorizer() authorizer, err := authzInfo.autorestAuthorizer()
if err != nil { if err != nil {
return nil, fmt.Errorf("error fetching autorest authorizer: %w", err) return nil, fmt.Errorf("error fetching autorest authorizer: %w", err)
@@ -171,16 +307,13 @@ func (p *AzurePlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (
return nil, errors.New("fetched authorizer is nil") return nil, errors.New("fetched authorizer is nil")
} }
commonOpts := make([]Option, 0, 3) // Set up common client options
commonOpts = append(commonOpts, commonOpts := []Option{
WithSubscriptionId(authzInfo.AuthParams.SubscriptionId), WithSubscriptionId(authzInfo.AuthParams.SubscriptionId),
WithAuthorizer(authorizer), WithAuthorizer(authorizer),
) }
// Getting authorizer will fail if the attr fields are not populated, so // Add base URL if specified
// it's safe to access directly at this point.
//
// TODO: plumb base URL everywhere, not just here
catalogAttrFields := catalogAttrs.GetFields() catalogAttrFields := catalogAttrs.GetFields()
if catalogAttrFields[constBaseUrl] != nil { if catalogAttrFields[constBaseUrl] != nil {
if baseUrl := catalogAttrFields[constBaseUrl].GetStringValue(); baseUrl != "" { if baseUrl := catalogAttrFields[constBaseUrl].GetStringValue(); baseUrl != "" {
@@ -188,229 +321,137 @@ func (p *AzurePlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (
} }
} }
// Create clients // Initialize clients
resClient, err := getResourcesClient(commonOpts...) clients := &azureClients{}
if err != nil { var initErr error
return nil, fmt.Errorf("error fetching resources client: %w", err)
} // Initialize individual clients with error handling
if resClient == nil { if clients.resClient, initErr = getResourcesClient(commonOpts...); initErr != nil {
return nil, errors.New("resource client is nil") return nil, fmt.Errorf("error fetching resources client: %w", initErr)
}
vmClient, err := getVirtualMachinesClient(commonOpts...)
if err != nil {
return nil, fmt.Errorf("error fetching virtual machines client: %w", err)
}
if vmClient == nil {
return nil, errors.New("virtual machines client is nil")
}
ifClient, err := getNetworkInterfacesClient(commonOpts...)
if err != nil {
return nil, fmt.Errorf("error fetching network interfaces client: %w", err)
}
if ifClient == nil {
return nil, errors.New("network interfaces client is nil")
}
pipClient, err := getPublicIpAddressesClient(commonOpts...)
if err != nil {
return nil, fmt.Errorf("error fetching public ip address client: %w", err)
}
if pipClient == nil {
return nil, errors.New("public ip address client is nil")
} }
// This next section finds resource IDs that match to the filter in each if clients.vmClient, initErr = getVirtualMachinesClient(commonOpts...); initErr != nil {
// host set, and stores a mapping of, for each resource ID, which host sets return nil, fmt.Errorf("error fetching virtual machines client: %w", initErr)
// it belongs to
resourceToSetMap := make(map[string][]string, len(req.GetSets())*10)
var resourceInfos []resources.GenericResourceExpanded
for _, set := range req.GetSets() {
if set == nil {
return nil, errors.New("set is nil")
}
setAttrs := set.GetAttributes()
if setAttrs == nil {
return nil, fmt.Errorf("set %s has no attributes", set.GetId())
}
setAttrFields := setAttrs.GetFields()
if len(setAttrFields) == 0 {
return nil, fmt.Errorf("set %s attributes has no filter", set.GetId())
}
filter := constDefaultFilter
if setAttrFields[constFilter] != nil {
if filter = setAttrFields[constFilter].GetStringValue(); filter == "" {
return nil, fmt.Errorf("set %s filter is empty", set.GetId())
}
}
// List values matching the filter; ask for provisioning state information
// to ensure we are seeing only fully provisioned machines
{
iter, err := resClient.ListComplete(ctx, filter, "", nil)
if err != nil {
return nil, fmt.Errorf("error listing resources: %w", err)
}
for iter.NotDone() {
val := iter.Value()
if err := iter.NextWithContext(ctx); err != nil {
return nil, fmt.Errorf("error iterating resources: %w", err)
}
if val.ID == nil { // something went wrong, likely iterator has advanced beyond the end
continue
}
if val.Type == nil || !strings.EqualFold(*val.Type, "Microsoft.Compute/virtualMachines") {
// no point continuing if we can't validate that it's a VM
continue
}
resourceInfos = append(resourceInfos, val)
resourceToSetMap[*val.ID] = append(resourceToSetMap[*val.ID], set.GetId())
}
}
} }
// At this point we have a list of distinct resources and a mapping between if clients.vmssClient, initErr = getVirtualMachineScaleSetsClient(commonOpts...); initErr != nil {
// them and the set(s) they belong to. Iterate through them and fetch return nil, fmt.Errorf("error fetching virtual machine scale set client: %w", initErr)
// information.
//
// Listing returns IDs which are basically URLs that have key-value
// pairs but not the individual components. Get requires the individual
// components, and won't accept the ID...
//
// Anyways, now we find VM details for those resources
vmToIfaceMap := make(map[string][]network.Interface)
{
for _, res := range resourceInfos {
resourceGroup, name, err := splitId(*res.ID, constMsComputeService, constVirtualMachinesResource)
if err != nil {
return nil, fmt.Errorf("error splitting vm id %q: %w", *res.ID, err)
}
vm, err := vmClient.Get(ctx, resourceGroup, name, "")
if err != nil {
return nil, fmt.Errorf("error fetching vm with id %q: %w", *res.ID, err)
}
iv, err := vmClient.InstanceView(ctx, resourceGroup, name)
if err != nil {
return nil, fmt.Errorf("error fetching instance view for vm with id %q: %w", *res.ID, err)
}
if iv.Statuses == nil {
return nil, fmt.Errorf("instance view statuses returned for vm with id %q is null", *res.ID)
}
var running bool
for _, s := range *iv.Statuses {
if s.Code == nil {
continue
}
state := strings.ToLower(*s.Code)
prefix := "powerstate/"
if !strings.HasPrefix(state, prefix) {
continue
}
state = strings.TrimPrefix(state, prefix)
if state == "running" {
running = true
break
}
}
if !running {
continue
}
props := vm.VirtualMachineProperties
if props == nil {
return nil, fmt.Errorf("error fetching properties for vm with id %q: %w", *res.ID, err)
}
if props.NetworkProfile == nil {
return nil, fmt.Errorf("error fetching network profile for vm with id %q", *res.ID)
}
// Within the VM, catalog the various interfaces
var ifaces []network.Interface
for _, ifaceRef := range *props.NetworkProfile.NetworkInterfaces {
if ifaceRef.ID == nil {
return nil, fmt.Errorf("nil ID for network interface for vm with id %q", *res.ID)
}
ifResGroup, ifName, err := splitId(*ifaceRef.ID, constMsNetworkService, constNetworkInterfacesResource)
if err != nil {
return nil, fmt.Errorf("error splitting network interface id %q: %w", *ifaceRef.ID, err)
}
iface, err := ifClient.Get(ctx, ifResGroup, ifName, "")
if err != nil {
return nil, fmt.Errorf("error fetching network interface with id %q: %w", *ifaceRef.ID, err)
}
if iface.InterfacePropertiesFormat == nil || iface.InterfacePropertiesFormat.IPConfigurations == nil {
continue
}
ifaces = append(ifaces, iface)
}
vmToIfaceMap[*res.ID] = ifaces
}
} }
// Now, fetch IP details for each of the interfaces for each VM if clients.vmssvmClient, initErr = getVirtualMachineScaleSetVMsClient(commonOpts...); initErr != nil {
type networkInfo struct { return nil, fmt.Errorf("error fetching virtual machine scale set vms client: %w", initErr)
IpAddresses []string
}
vmToNetworkMap := make(map[string]networkInfo)
{
for vmId, ifaces := range vmToIfaceMap {
var netInfo networkInfo
for _, iface := range ifaces {
for _, ipconf := range *iface.InterfacePropertiesFormat.IPConfigurations {
if ipconf.PrivateIPAddress != nil {
netInfo.IpAddresses = append(netInfo.IpAddresses, *ipconf.PrivateIPAddress)
}
if ipconf.PublicIPAddress != nil {
if ipconf.PublicIPAddress.ID == nil {
return nil, fmt.Errorf("ip configuration %q has public IP address info but nil id", *ipconf.ID)
}
ipResGroup, ipName, err := splitId(*ipconf.PublicIPAddress.ID, constMsNetworkService, constPublicIpAddressesResource)
if err != nil {
return nil, fmt.Errorf("error splitting public ip address id %q: %w", *ipconf.PublicIPAddress.ID, err)
}
pipInfo, err := pipClient.Get(ctx, ipResGroup, ipName, "")
if err != nil {
if err != nil {
return nil, fmt.Errorf("error fetching public IP information with resource group %q and name %q: %w", ipResGroup, ipName, err)
}
}
if pipInfo.PublicIPAddressPropertiesFormat == nil {
return nil, fmt.Errorf("nil public ip address properties format for public ip %q", *ipconf.PublicIPAddress.ID)
}
if pipInfo.PublicIPAddressPropertiesFormat.IPAddress != nil {
netInfo.IpAddresses = append(netInfo.IpAddresses, *pipInfo.PublicIPAddressPropertiesFormat.IPAddress)
}
}
}
}
vmToNetworkMap[vmId] = netInfo
}
} }
ret := &pb.ListHostsResponse{} if clients.ifClient, initErr = getNetworkInterfacesClient(commonOpts...); initErr != nil {
for k, v := range vmToNetworkMap { return nil, fmt.Errorf("error fetching network interfaces client: %w", initErr)
_, externalName, err := splitId(k, constMsComputeService, constVirtualMachinesResource)
if err != nil {
externalName = ""
}
host := &pb.ListHostsResponseHost{
ExternalId: k,
ExternalName: externalName,
IpAddresses: v.IpAddresses,
SetIds: resourceToSetMap[k],
}
ret.Hosts = append(ret.Hosts, host)
} }
return ret, nil
if clients.pipClient, initErr = getPublicIpAddressesClient(commonOpts...); initErr != nil {
return nil, fmt.Errorf("error fetching public ip address client: %w", initErr)
}
return clients, nil
} }
func (p *AzurePlugin) findMatchingResources(ctx context.Context,
sets []*hostsets.HostSet,
resClient *resources.Client) (
vmResources []resources.GenericResourceExpanded,
vmssResources []resources.GenericResourceExpanded,
resourceToSetMap map[string][]string,
_ error) {
resourceToSetMap = make(map[string][]string, len(sets)*10) // Pre-allocate map size
for _, set := range sets {
if err := validateHostSet(set); err != nil {
return nil, nil, nil, err
}
filter := getSetFilter(set)
setVMResources, setVMSSResources, setResourceToSetMap, err := listAndFilterResources(ctx, resClient, filter, set.GetId())
if err != nil {
return nil, nil, nil, err
}
vmResources = append(vmResources, setVMResources...)
vmssResources = append(vmssResources, setVMSSResources...)
for k, v := range setResourceToSetMap {
resourceToSetMap[k] = append(resourceToSetMap[k], v...)
}
}
return vmResources, vmssResources, resourceToSetMap, nil
}
func listAndFilterResources(ctx context.Context,
resClient *resources.Client,
filter, setID string) (
vmResources []resources.GenericResourceExpanded,
vmssResources []resources.GenericResourceExpanded,
resourceToSetMap map[string][]string,
_ error) {
resourceToSetMap = make(map[string][]string)
iter, err := resClient.ListComplete(ctx, filter, "", nil)
if err != nil {
return nil, nil, nil, fmt.Errorf("error listing resources: %w", err)
}
for iter.NotDone() {
val := iter.Value()
if err := iter.NextWithContext(ctx); err != nil {
return nil, nil, nil, fmt.Errorf("error iterating resources: %w", err)
}
if val.ID == nil || val.Type == nil {
continue
}
virtualMachineType := constMsComputeService + "/" + constVirtualMachinesResource
virtualMachineScaleSetType := constMsComputeService + "/" + constVirtualMachineScaleSetsResource
if strings.EqualFold(*val.Type, virtualMachineType) {
vmResources = append(vmResources, val)
resourceToSetMap[*val.ID] = append(resourceToSetMap[*val.ID], setID)
} else if strings.EqualFold(*val.Type, virtualMachineScaleSetType) {
vmssResources = append(vmssResources, val)
resourceToSetMap[*val.ID] = append(resourceToSetMap[*val.ID], setID)
}
}
return vmResources, vmssResources, resourceToSetMap, nil
}
// Resource Filter Management
func getSetFilter(set *hostsets.HostSet) string {
setAttrFields := set.GetAttributes().GetFields()
if setAttrFields[constFilter] != nil {
if filter := setAttrFields[constFilter].GetStringValue(); filter != "" {
return filter
}
}
return ""
}
func validateHostSet(set *hostsets.HostSet) error {
if set == nil {
return errors.New("set is nil")
}
setAttrs := set.GetAttributes()
if setAttrs == nil {
return fmt.Errorf("set %s has no attributes", set.GetId())
}
if len(setAttrs.GetFields()) == 0 {
return fmt.Errorf("set %s attributes has no filter", set.GetId())
}
return nil
}
// ID Parsing Functions
func splitId(in, expectedService, expectedResource string) (string, string, error) { func splitId(in, expectedService, expectedResource string) (string, string, error) {
// Note: this could be stolen from the helpers directory in the TF AzureRM // Note: this could be stolen from the helpers directory in the TF AzureRM
// provider but I'm not at the moment because it does a bunch of trickery // provider but I'm not at the moment because it does a bunch of trickery
// for some Azure APIs and we have a much more limited scope. I may come to // for some Azure APIs and we have a much more limited scope.
// regret this.
splitId := strings.Split(strings.TrimLeft(in, "/"), "/") splitId := strings.Split(strings.TrimLeft(in, "/"), "/")
// Run through some sanity checks // Run through some sanity checks
if len(splitId) != 8 || if len(splitId) != 8 ||
!strings.EqualFold(splitId[0], constSubscriptions) || !strings.EqualFold(splitId[0], constSubscriptions) ||
@@ -423,47 +464,39 @@ func splitId(in, expectedService, expectedResource string) (string, string, erro
return splitId[3], splitId[7], nil return splitId[3], splitId[7], nil
} }
// Returns an invalid argument error with the problematic fields included func extractResourceType(resourceID string) (string, error) {
// in the error message. if resourceID == "" {
func invalidArgumentError(msg string, f map[string]string) error { return "", fmt.Errorf("empty resource ID provided")
var fieldMsgs []string
for field, val := range f {
fieldMsgs = append(fieldMsgs, fmt.Sprintf("%q: %q", field, val))
} }
if len(fieldMsgs) > 0 {
msg = fmt.Sprintf("%s: [%s]", msg, strings.Join(fieldMsgs, ", ")) parts := strings.Split(strings.TrimLeft(resourceID, "/"), "/")
// Validate minimum length and basic structure
if len(parts) < 6 {
return "", fmt.Errorf("resource ID too short: %s", resourceID)
} }
return status.Error(codes.InvalidArgument, msg)
} // Check for required segments in correct positions
if !strings.EqualFold(parts[0], constSubscriptions) ||
func validateSecrets(s *structpb.Struct) error { !strings.EqualFold(parts[2], constResourceGroups) ||
if s == nil { !strings.EqualFold(parts[4], constProviders) {
return status.Error(codes.InvalidArgument, "Secrets not provided but are required") return "", fmt.Errorf("malformed resource ID, missing required segments: %s", resourceID)
} }
var secrets SecretData
if err := mapstructure.Decode(s.AsMap(), &secrets); err != nil { // Check for Microsoft.Compute in correct position
return status.Errorf(codes.InvalidArgument, "error decoding catalog secrets: %s", err) if !strings.EqualFold(parts[5], constMsComputeService) {
} return "", nil // Not a compute resource, return empty string without error
badFields := make(map[string]string) }
if len(secrets.SecretValue) == 0 {
badFields["secrets.secret_value"] = "This field is required." // Ensure there's a resource type after Microsoft.Compute
} if len(parts) < 7 {
if len(secrets.CredsLastRotatedTime) != 0 { return "", fmt.Errorf("no resource type found after Microsoft.Compute: %s", resourceID)
badFields["secrets.creds_last_rotated_time"] = "This field is reserved and cannot be set." }
}
if len(badFields) > 0 { return parts[6], nil
return invalidArgumentError("Error in the secrets provided", badFields)
}
return nil
}
var allowedCatalogFields = map[string]struct{}{
constDisableCredentialRotation: {},
constSubscriptionId: {},
constTenantId: {},
constClientId: {},
} }
// Validation Functions
func validateCatalog(c *hostcatalogs.HostCatalog) error { func validateCatalog(c *hostcatalogs.HostCatalog) error {
if c == nil { if c == nil {
return status.Error(codes.InvalidArgument, "catalog is nil") return status.Error(codes.InvalidArgument, "catalog is nil")
@@ -473,6 +506,7 @@ func validateCatalog(c *hostcatalogs.HostCatalog) error {
if err := mapstructure.Decode(attrMap, &attrs); err != nil { if err := mapstructure.Decode(attrMap, &attrs); err != nil {
return status.Errorf(codes.InvalidArgument, "error decoding catalog attributes: %s", err) return status.Errorf(codes.InvalidArgument, "error decoding catalog attributes: %s", err)
} }
badFields := make(map[string]string) badFields := make(map[string]string)
if !attrs.DisableCredentialRotation { if !attrs.DisableCredentialRotation {
badFields["attributes.disable_credential_rotation"] = "This field must be set to true." badFields["attributes.disable_credential_rotation"] = "This field must be set to true."
@@ -499,8 +533,26 @@ func validateCatalog(c *hostcatalogs.HostCatalog) error {
return nil return nil
} }
var allowedSetFields = map[string]struct{}{ func validateSecrets(s *structpb.Struct) error {
constFilter: {}, if s == nil {
return status.Error(codes.InvalidArgument, "Secrets not provided but are required")
}
var secrets SecretData
if err := mapstructure.Decode(s.AsMap(), &secrets); err != nil {
return status.Errorf(codes.InvalidArgument, "error decoding catalog secrets: %s", err)
}
badFields := make(map[string]string)
if len(secrets.SecretValue) == 0 {
badFields["secrets.secret_value"] = "This field is required."
}
if len(secrets.CredsLastRotatedTime) != 0 {
badFields["secrets.creds_last_rotated_time"] = "This field is reserved and cannot be set."
}
if len(badFields) > 0 {
return invalidArgumentError("Error in the secrets provided", badFields)
}
return nil
} }
func validateSet(s *hostsets.HostSet) error { func validateSet(s *hostsets.HostSet) error {
@@ -512,6 +564,7 @@ func validateSet(s *hostsets.HostSet) error {
if err := mapstructure.Decode(attrMap, &attrs); err != nil { if err := mapstructure.Decode(attrMap, &attrs); err != nil {
return status.Errorf(codes.InvalidArgument, "error decoding set attributes: %s", err) return status.Errorf(codes.InvalidArgument, "error decoding set attributes: %s", err)
} }
badFields := make(map[string]string) badFields := make(map[string]string)
if _, ok := attrMap[constFilter]; ok && len(attrs.Filter) == 0 { if _, ok := attrMap[constFilter]; ok && len(attrs.Filter) == 0 {
badFields["attributes.filter"] = "This field must be not empty." badFields["attributes.filter"] = "This field must be not empty."
@@ -528,3 +581,27 @@ func validateSet(s *hostsets.HostSet) error {
} }
return nil return nil
} }
// Error Handling Functions
func invalidArgumentError(msg string, f map[string]string) error {
var fieldMsgs []string
for field, val := range f {
fieldMsgs = append(fieldMsgs, fmt.Sprintf("%q: %q", field, val))
}
if len(fieldMsgs) > 0 {
msg = fmt.Sprintf("%s: [%s]", msg, strings.Join(fieldMsgs, ", "))
}
return status.Error(codes.InvalidArgument, msg)
}
// Map Definitions for Allowed Fields
var allowedCatalogFields = map[string]struct{}{
constDisableCredentialRotation: {},
constSubscriptionId: {},
constTenantId: {},
constClientId: {},
}
var allowedSetFields = map[string]struct{}{
constFilter: {},
}
+288 -2
View File
@@ -7,6 +7,8 @@ import (
"context" "context"
"os" "os"
"path/filepath" "path/filepath"
"reflect"
"strings"
"testing" "testing"
"time" "time"
@@ -448,7 +450,7 @@ func testGetHostStructs(t *testing.T) (*hostcatalogs.HostCatalog, []*hostsets.Ho
Id: "set1", Id: "set1",
Attrs: &hostsets.HostSet_Attributes{ Attrs: &hostsets.HostSet_Attributes{
Attributes: wrapMap(t, map[string]interface{}{ Attributes: wrapMap(t, map[string]interface{}{
constFilter: constDefaultFilter, constFilter: "",
}), }),
}, },
}, },
@@ -456,7 +458,7 @@ func testGetHostStructs(t *testing.T) (*hostcatalogs.HostCatalog, []*hostsets.Ho
Id: "set2", Id: "set2",
Attrs: &hostsets.HostSet_Attributes{ Attrs: &hostsets.HostSet_Attributes{
Attributes: wrapMap(t, map[string]interface{}{ Attributes: wrapMap(t, map[string]interface{}{
constFilter: constDefaultFilter, constFilter: "",
}), }),
}, },
}, },
@@ -505,3 +507,287 @@ func testCreateInitialSecret(t *testing.T, hostCatalog *hostcatalogs.HostCatalog
return authzInfo, initialCred, cleanup return authzInfo, initialCred, cleanup
} }
func TestSplitId(t *testing.T) {
tests := []struct {
name string
input string
expectedService string
expectedResource string
wantRG string
wantName string
wantErr bool
}{
{
name: "valid resource id",
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/vm1",
expectedService: "Microsoft.Compute",
expectedResource: "virtualMachines",
wantRG: "rg1",
wantName: "vm1",
wantErr: false,
},
{
name: "valid with different case",
input: "/SUBSCRIPTIONS/sub1/RESOURCEGROUPS/rg1/PROVIDERS/MICROSOFT.COMPUTE/VIRTUALMACHINES/vm1",
expectedService: "Microsoft.Compute",
expectedResource: "virtualMachines",
wantRG: "rg1",
wantName: "vm1",
wantErr: false,
},
{
name: "invalid - trailing slash",
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/vm1/",
expectedService: "Microsoft.Compute",
expectedResource: "virtualMachines",
wantRG: "",
wantName: "",
wantErr: true,
},
{
name: "invalid - wrong service",
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Storage/virtualMachines/vm1",
expectedService: "Microsoft.Compute",
expectedResource: "virtualMachines",
wantRG: "",
wantName: "",
wantErr: true,
},
{
name: "invalid - wrong resource type",
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/diskEncryptionSets/des1",
expectedService: "Microsoft.Compute",
expectedResource: "virtualMachines",
wantRG: "",
wantName: "",
wantErr: true,
},
{
name: "invalid - missing segments",
input: "/subscriptions/sub1/resourceGroups/rg1",
expectedService: "Microsoft.Compute",
expectedResource: "virtualMachines",
wantRG: "",
wantName: "",
wantErr: true,
},
{
name: "invalid - empty string",
input: "",
expectedService: "Microsoft.Compute",
expectedResource: "virtualMachines",
wantRG: "",
wantName: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotRG, gotName, err := splitId(tt.input, tt.expectedService, tt.expectedResource)
if (err != nil) != tt.wantErr {
t.Errorf("splitId() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotRG != tt.wantRG {
t.Errorf("splitId() gotRG = %v, want %v", gotRG, tt.wantRG)
}
if gotName != tt.wantName {
t.Errorf("splitId() gotName = %v, want %v", gotName, tt.wantName)
}
})
}
}
func TestExtractResourceType(t *testing.T) {
tests := []struct {
name string
input string
want string
wantErr bool
}{
{
name: "valid compute resource",
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/vm1",
want: "virtualMachines",
wantErr: false,
},
{
name: "valid compute resource different case",
input: "/subscriptions/sub1/resourceGroups/rg1/providers/MICROSOFT.COMPUTE/virtualMachines/vm1",
want: "virtualMachines",
wantErr: false,
},
{
name: "valid with trailing slash",
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/vm1/",
want: "virtualMachines",
wantErr: false,
},
{
name: "different compute resource type",
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/diskEncryptionSets/des1",
want: "diskEncryptionSets",
wantErr: false,
},
{
name: "non-compute resource",
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Storage/storageAccounts/sa1",
want: "",
wantErr: false,
},
{
name: "malformed - missing provider",
input: "/subscriptions/sub1/resourceGroups/rg1/Microsoft.Compute/virtualMachines/vm1",
want: "",
wantErr: true,
},
{
name: "malformed - missing resource type",
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute",
want: "",
wantErr: true,
},
{
name: "empty string",
input: "",
want: "",
wantErr: true,
},
{
name: "too short",
input: "/subscriptions/sub1",
want: "",
wantErr: true,
},
{
name: "malformed structure",
input: "/something/else/entirely",
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := extractResourceType(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("extractResourceType() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("extractResourceType() = %v, want %v", got, tt.want)
}
})
}
}
func TestCreateHostFromResource(t *testing.T) {
// Setup common test data
testNetwork := networkInfo{
IpAddresses: []string{"192.168.1.1", "192.168.1.2"},
}
// Use full Azure resource paths as keys
vmssPath := "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets/vmss1"
vmPath := "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/vm-resource1"
resourceToSetMap := map[string][]string{
vmssPath: {"set1", "set2"},
vmPath: {"set3", "set4"},
}
tests := []struct {
name string
resourceID string
network networkInfo
setMap map[string][]string
want *pb.ListHostsResponseHost
wantErr bool
errMessage string
}{
{
name: "valid VMSS resource",
resourceID: vmssPath + "/virtualMachines/vm1",
network: testNetwork,
setMap: resourceToSetMap,
want: &pb.ListHostsResponseHost{
ExternalId: vmssPath + "/virtualMachines/vm1",
ExternalName: "vmss1_vm1",
IpAddresses: []string{"192.168.1.1", "192.168.1.2"},
SetIds: []string{"set1", "set2"},
},
wantErr: false,
},
{
name: "valid VM resource",
resourceID: vmPath,
network: testNetwork,
setMap: resourceToSetMap,
want: &pb.ListHostsResponseHost{
ExternalId: vmPath,
ExternalName: "vm-resource1",
IpAddresses: []string{"192.168.1.1", "192.168.1.2"},
SetIds: []string{"set3", "set4"},
},
wantErr: false,
},
{
name: "invalid resource ID",
resourceID: "invalid-resource-id",
network: testNetwork,
setMap: resourceToSetMap,
want: nil,
wantErr: true,
errMessage: "invalid resource ID",
},
{
name: "unsupported resource type",
resourceID: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/unsupportedResource/resource1",
network: testNetwork,
setMap: resourceToSetMap,
want: nil,
wantErr: true,
errMessage: "unsupported resource type",
},
{
name: "VMSS with valid resource type but invalid structure",
resourceID: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets", // Missing required segments after virtualMachineScaleSets
network: testNetwork,
setMap: resourceToSetMap,
want: nil,
wantErr: true,
errMessage: "could not determine set for resource ID: not a valid VMSS resource ID",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := createHostFromResource(tt.resourceID, tt.network, tt.setMap)
// Check error cases
if tt.wantErr {
if err == nil {
t.Errorf("createHostFromResource() expected error containing %q, got nil", tt.errMessage)
return
}
if !strings.Contains(err.Error(), tt.errMessage) {
t.Errorf("createHostFromResource() error = %v, want error containing %q", err, tt.errMessage)
}
return
}
// Check non-error cases
if err != nil {
t.Errorf("createHostFromResource() unexpected error: %v", err)
return
}
// Compare the results
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("createHostFromResource() = %v, want %v", got, tt.want)
}
})
}
}
+131
View File
@@ -0,0 +1,131 @@
package plugin
import (
"context"
"fmt"
"strings"
"sync"
"github.com/Azure/azure-sdk-for-go/profiles/latest/compute/mgmt/compute"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/resources"
"golang.org/x/sync/errgroup"
)
// vmJob represents a single VM processing job
type vmJob struct {
resource resources.GenericResourceExpanded
resourceGroup string
name string
}
func (p *AzurePlugin) processStandardVMs(ctx context.Context, resourceInfos []resources.GenericResourceExpanded,
clients *azureClients) (map[string]networkInfo, error) {
// Initialize the map that we'll return
vmToNetworkMap := make(map[string]networkInfo)
// Pre-validate all resource IDs and create jobs slice
jobs := make([]vmJob, 0, len(resourceInfos))
for _, res := range resourceInfos {
resourceGroup, name, err := splitId(*res.ID, constMsComputeService, constVirtualMachinesResource)
if err != nil {
return nil, fmt.Errorf("invalid vm id %q: %w", *res.ID, err)
}
jobs = append(jobs, vmJob{
resource: res,
resourceGroup: resourceGroup,
name: name,
})
}
// Create error group for worker management
g, ctx := errgroup.WithContext(ctx)
// Create job channel
jobsChan := make(chan vmJob)
// Result handling
mu := &sync.Mutex{}
// Start worker pool
for i := 0; i < constDefaultWorkers; i++ {
g.Go(func() error {
for job := range jobsChan {
// Process single VM
vm, err := clients.vmClient.Get(ctx, job.resourceGroup, job.name, "")
if err != nil {
return fmt.Errorf("error fetching vm with id %q: %w", *job.resource.ID, err)
}
// Only process running VMs
running, err := isVMRunning(ctx, clients.vmClient, job.resourceGroup, job.name)
if err != nil {
return err
}
if !running {
continue
}
var netInfo networkInfo
if err := processVMNetworkInterfaces(ctx, vm, clients, job.resourceGroup, &netInfo); err != nil {
return err
}
mu.Lock()
vmToNetworkMap[*job.resource.ID] = netInfo
mu.Unlock()
}
return nil
})
}
// Send jobs to workers
go func() {
defer close(jobsChan)
for _, job := range jobs {
// Check context cancellation
if ctx.Err() != nil {
return
}
jobsChan <- job
}
}()
// Wait for all workers to complete
if err := g.Wait(); err != nil {
return nil, err
}
return vmToNetworkMap, nil
}
// VM Status Check
func isVMRunning(
ctx context.Context,
vmClient *compute.VirtualMachinesClient,
resourceGroup,
name string,
) (
bool,
error,
) {
iv, err := vmClient.InstanceView(ctx, resourceGroup, name)
if err != nil {
return false, fmt.Errorf("error fetching instance view for vm %s: %w", name, err)
}
if iv.Statuses == nil {
return false, fmt.Errorf("instance view statuses is null for vm %s", name)
}
for _, s := range *iv.Statuses {
if s.Code == nil {
continue
}
state := strings.ToLower(*s.Code)
if strings.HasPrefix(state, "powerstate/") && strings.TrimPrefix(state, "powerstate/") == "running" {
return true, nil
}
}
return false, nil
}
+302
View File
@@ -0,0 +1,302 @@
package plugin
import (
"context"
"fmt"
"strings"
"sync"
"github.com/Azure/azure-sdk-for-go/profiles/latest/compute/mgmt/compute"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/resources"
"golang.org/x/sync/errgroup"
)
// vmssJob represents a single VMSS processing job
type vmssJob struct {
resource resources.GenericResourceExpanded
resourceGroup string
name string
}
// vmssInstanceJob represents a single VMSS instance processing job
type vmssInstanceJob struct {
vmssvm compute.VirtualMachineScaleSetVM
resourceGroup string
vmssName string
}
func (p *AzurePlugin) processVMScaleSets(
ctx context.Context,
resourceInfos []resources.GenericResourceExpanded,
clients *azureClients,
) (
map[string]networkInfo,
error,
) {
// Initialize the map that we'll return
vmToNetworkMap := make(map[string]networkInfo)
// Pre-validate all resource IDs and create jobs slice
jobs := make([]vmssJob, 0, len(resourceInfos))
for _, res := range resourceInfos {
resourceGroup, name, err := splitId(*res.ID, constMsComputeService, constVirtualMachineScaleSetsResource)
if err != nil {
return nil, fmt.Errorf("invalid vmss id %q: %w", *res.ID, err)
}
jobs = append(jobs, vmssJob{
resource: res,
resourceGroup: resourceGroup,
name: name,
})
}
// Create error group for worker management
g, ctx := errgroup.WithContext(ctx)
// Create job channel
jobsChan := make(chan vmssJob)
// Start worker pool
for i := 0; i < constDefaultWorkers; i++ {
g.Go(func() error {
for job := range jobsChan {
vmss, err := clients.vmssClient.Get(ctx, job.resourceGroup, job.name, "")
if err != nil {
return fmt.Errorf("error fetching vmss with id %q: %w", *job.resource.ID, err)
}
// Check if VMSS properties exist
if vmss.VirtualMachineScaleSetProperties == nil {
continue
}
vmssProperties := vmss.VirtualMachineScaleSetProperties
// Skip if OrchestrationMode is empty or Flexible
if vmssProperties.OrchestrationMode == "" {
continue
}
if vmssProperties.OrchestrationMode == compute.OrchestrationModeFlexible {
continue
}
if err := processVMSSInstancesParallel(ctx, vmss, job.resourceGroup, clients, vmToNetworkMap); err != nil {
return fmt.Errorf("error processing uniform vmss instances: %w", err)
}
}
return nil
})
}
// Send jobs to workers
go func() {
defer close(jobsChan)
for _, job := range jobs {
if ctx.Err() != nil {
return
}
jobsChan <- job
}
}()
// Wait for all workers to complete
if err := g.Wait(); err != nil {
return nil, err
}
return vmToNetworkMap, nil
}
func processVMSSInstancesParallel(
ctx context.Context,
vmss compute.VirtualMachineScaleSet,
resourceGroup string,
clients *azureClients,
vmToNetworkMap map[string]networkInfo,
) error {
if vmss.Name == nil {
return fmt.Errorf("vmss name is nil")
}
vmssvms, err := clients.vmssvmClient.List(ctx, resourceGroup, *vmss.Name, "", "", "")
if err != nil {
return fmt.Errorf("error fetching vms for vmss %s: %w", *vmss.Name, err)
}
// Create new error group for instance processing
g, ctx := errgroup.WithContext(ctx)
instanceJobs := make(chan vmssInstanceJob)
// Start instance workers
instanceWorkerCount := constDefaultWorkers
if len(vmssvms.Values()) < instanceWorkerCount {
instanceWorkerCount = len(vmssvms.Values())
}
mu := &sync.Mutex{}
// Start instance worker pool
for i := 0; i < instanceWorkerCount; i++ {
g.Go(func() error {
for job := range instanceJobs {
if job.vmssvm.ID == nil {
return fmt.Errorf("nil ID for vmss vm in scale set %s", *vmss.Name)
}
if err := processVMSSInstanceParallel(ctx, job.vmssvm, job.resourceGroup,
job.vmssName, clients, vmToNetworkMap, mu); err != nil {
return fmt.Errorf("error processing vmss instance %s: %w", *job.vmssvm.ID, err)
}
}
return nil
})
}
// Send instance jobs
go func() {
defer close(instanceJobs)
for _, vmssvm := range vmssvms.Values() {
select {
case <-ctx.Done():
return
default:
}
instanceJobs <- vmssInstanceJob{
vmssvm: vmssvm,
resourceGroup: resourceGroup,
vmssName: *vmss.Name,
}
}
}()
return g.Wait()
}
func processVMSSInstanceParallel(
ctx context.Context,
vmssvm compute.VirtualMachineScaleSetVM,
resourceGroup,
vmssName string,
clients *azureClients,
vmToNetworkMap map[string]networkInfo,
mu *sync.Mutex,
) error {
if vmssvm.InstanceID == nil {
return fmt.Errorf("instance ID is nil")
}
// Check if instance is running
if running, err := isVMSSInstanceRunning(ctx, clients.vmssvmClient, resourceGroup,
vmssName, vmssvm.InstanceID); err != nil {
return err
} else if !running {
return nil
}
// Process network interfaces for the instance
var netInfo networkInfo
if err := processVMSSNetworkInterfaces(ctx, vmssvm, resourceGroup, vmssName, clients, &netInfo); err != nil {
return err
}
func() {
mu.Lock()
defer mu.Unlock()
vmToNetworkMap[*vmssvm.ID] = netInfo
}()
return nil
}
// VMSS Instance Status Check
func isVMSSInstanceRunning(
ctx context.Context,
vmssvmClient *compute.VirtualMachineScaleSetVMsClient,
resourceGroup,
vmssName string,
instanceID *string,
) (
bool,
error,
) {
if instanceID == nil {
return false, fmt.Errorf("instance ID is nil")
}
iv, err := vmssvmClient.GetInstanceView(ctx, resourceGroup, vmssName, *instanceID)
if err != nil {
return false, fmt.Errorf("error fetching instance view for instance %s: %w", *instanceID, err)
}
if iv.Statuses == nil {
return false, fmt.Errorf("instance view statuses returned for instance %s is null", *instanceID)
}
for _, s := range *iv.Statuses {
if s.Code == nil {
continue
}
state := strings.ToLower(*s.Code)
prefix := "powerstate/"
if !strings.HasPrefix(state, prefix) {
continue
}
state = strings.TrimPrefix(state, prefix)
if state == "running" {
return true, nil
}
}
return false, nil
}
// Helper Functions
func getSetForVMSSInstance(in string) (string, error) {
if in == "" {
return "", fmt.Errorf("empty resource ID provided")
}
// Split the path into segments
segments := strings.Split(strings.TrimLeft(in, "/"), "/")
// Check if this is a VMSS path by looking for required segments
if len(segments) < 8 ||
!strings.EqualFold(segments[0], constSubscriptions) ||
!strings.EqualFold(segments[2], constResourceGroups) ||
!strings.EqualFold(segments[4], constProviders) ||
!strings.EqualFold(segments[5], constMsComputeService) ||
!strings.EqualFold(segments[6], constVirtualMachineScaleSetsResource) {
return "", fmt.Errorf("not a valid VMSS resource ID: %s", in)
}
// Reconstruct the VMSS path (everything up to but not including virtualMachines)
vmssPath := "/" + strings.Join(segments[:8], "/")
// Additional validation: if the path contains virtualMachines, it should be in the correct position
for i, segment := range segments[8:] {
if strings.EqualFold(segment, constVirtualMachinesResource) {
if i != 0 { // virtualMachines should be immediately after VMSS name
return "", fmt.Errorf("unexpected virtualMachines segment position in resource ID: %s", in)
}
break
}
}
return vmssPath, nil
}
func getExternalNameforVMSSInstance(in string) (string, error) {
splitId := strings.Split(strings.TrimLeft(in, "/"), "/")
if len(splitId) != 10 ||
!strings.EqualFold(splitId[0], constSubscriptions) ||
!strings.EqualFold(splitId[2], constResourceGroups) ||
!strings.EqualFold(splitId[4], constProviders) ||
!strings.EqualFold(splitId[5], constMsComputeService) ||
!strings.EqualFold(splitId[6], constVirtualMachineScaleSetsResource) ||
!strings.EqualFold(splitId[8], constVirtualMachinesResource) {
return "", fmt.Errorf("unexpected format of virtual machine stateful set ID: %v", splitId)
}
return splitId[7] + "_" + splitId[9], nil
}
+153
View File
@@ -0,0 +1,153 @@
package plugin
import (
"testing"
)
func TestGetExternalNameforVMSSInstance(t *testing.T) {
tests := []struct {
name string
input string
want string
wantErr bool
}{
{
name: "valid vmss instance id",
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets/vmss1/virtualMachines/instance1",
want: "vmss1_instance1",
wantErr: false,
},
{
name: "invalid - vmss instance id with trailing slash",
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets/vmss1/virtualMachines/instance1/",
want: "",
wantErr: true,
},
{
name: "case insensitive segment validation",
input: "/SUBSCRIPTIONS/sub1/RESOURCEGROUPS/rg1/PROVIDERS/MICROSOFT.COMPUTE/VIRTUALMACHINESCALESETS/vmss1/VIRTUALMACHINES/instance1",
want: "vmss1_instance1",
wantErr: false,
},
{
name: "invalid - missing segment",
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets/vmss1",
want: "",
wantErr: true,
},
{
name: "invalid - wrong resource type",
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Storage/storageAccounts/sa1",
want: "",
wantErr: true,
},
{
name: "invalid - empty string",
input: "",
want: "",
wantErr: true,
},
{
name: "invalid - malformed id",
input: "invalid/format/string",
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := getExternalNameforVMSSInstance(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("getExternalNameforVMSSInstance() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("getExternalNameforVMSSInstance() = %v, want %v", got, tt.want)
}
})
}
}
func TestGetSetForVMSSInstance(t *testing.T) {
tests := []struct {
name string
input string
want string
wantErr bool
}{
{
name: "valid vmss instance id",
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets/vmss1/virtualMachines/instance1",
want: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets/vmss1",
wantErr: false,
},
{
name: "valid vmss path without instance",
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets/vmss1",
want: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets/vmss1",
wantErr: false,
},
{
name: "case insensitive validation",
input: "/SUBSCRIPTIONS/sub1/RESOURCEGROUPS/rg1/PROVIDERS/MICROSOFT.COMPUTE/VIRTUALMACHINESCALESETS/vmss1",
want: "/SUBSCRIPTIONS/sub1/RESOURCEGROUPS/rg1/PROVIDERS/MICROSOFT.COMPUTE/VIRTUALMACHINESCALESETS/vmss1",
wantErr: false,
},
{
name: "path with trailing slash",
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets/vmss1/virtualMachines/instance1/",
want: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets/vmss1",
wantErr: false,
},
{
name: "empty string",
input: "",
want: "",
wantErr: true,
},
{
name: "invalid - flexible orchestration mode VM path",
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/vmss1_instance1",
want: "",
wantErr: true,
},
{
name: "invalid - wrong provider",
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Storage/virtualMachineScaleSets/vmss1",
want: "",
wantErr: true,
},
{
name: "invalid - malformed path missing segments",
input: "/subscriptions/sub1/resourceGroups",
want: "",
wantErr: true,
},
{
name: "invalid - malformed with virtualMachines in wrong position",
input: "/subscriptions/sub1/resourceGroups/rg1/virtualMachines/providers/Microsoft.Compute/virtualMachineScaleSets/vmss1",
want: "",
wantErr: true,
},
{
name: "invalid - random string",
input: "not/a/valid/path",
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := getSetForVMSSInstance(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("getSetForVMSSInstance() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("getSetForVMSSInstance() = %v, want %v", got, tt.want)
}
})
}
}