Skip to content

Commit 158793f

Browse files
authored
Add singular data source for retrieving a Python package from an Artifact Registry repository (#14611)
1 parent 488e4ed commit 158793f

File tree

4 files changed

+406
-0
lines changed

4 files changed

+406
-0
lines changed

mmv1/third_party/terraform/provider/provider_mmv1_resources.go.tmpl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ var handwrittenDatasources = map[string]*schema.Resource{
3131
"google_artifact_registry_locations": artifactregistry.DataSourceGoogleArtifactRegistryLocations(),
3232
"google_artifact_registry_npm_package": artifactregistry.DataSourceArtifactRegistryNpmPackage(),
3333
"google_artifact_registry_package": artifactregistry.DataSourceArtifactRegistryPackage(),
34+
"google_artifact_registry_python_package": artifactregistry.DataSourceArtifactRegistryPythonPackage(),
3435
"google_artifact_registry_repositories": artifactregistry.DataSourceArtifactRegistryRepositories(),
3536
"google_artifact_registry_repository": artifactregistry.DataSourceArtifactRegistryRepository(),
3637
"google_artifact_registry_tag": artifactregistry.DataSourceArtifactRegistryTag(),
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
package artifactregistry
2+
3+
import (
4+
"fmt"
5+
"net/url"
6+
"sort"
7+
"strings"
8+
"time"
9+
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
11+
"github.com/hashicorp/terraform-provider-google/google/tpgresource"
12+
transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport"
13+
)
14+
15+
type PythonPackage struct {
16+
name string
17+
packageName string
18+
version string
19+
createTime string
20+
updateTime string
21+
}
22+
23+
func DataSourceArtifactRegistryPythonPackage() *schema.Resource {
24+
return &schema.Resource{
25+
Read: DataSourceArtifactRegistryPythonPackageRead,
26+
27+
Schema: map[string]*schema.Schema{
28+
"project": {
29+
Type: schema.TypeString,
30+
Optional: true,
31+
Description: "Project ID of the project.",
32+
},
33+
"location": {
34+
Type: schema.TypeString,
35+
Required: true,
36+
Description: "The region of the Artifact Registry repository.",
37+
},
38+
"repository_id": {
39+
Type: schema.TypeString,
40+
Required: true,
41+
Description: "The repository ID containing the Python package.",
42+
},
43+
"package_name": {
44+
Type: schema.TypeString,
45+
Required: true,
46+
Description: "The name of the Python package.",
47+
},
48+
"version": {
49+
Type: schema.TypeString,
50+
Computed: true,
51+
Description: "The version of the Python package.",
52+
},
53+
"name": {
54+
Type: schema.TypeString,
55+
Computed: true,
56+
Description: "The fully qualified name of the Python package.",
57+
},
58+
"create_time": {
59+
Type: schema.TypeString,
60+
Computed: true,
61+
Description: "The time the package was created.",
62+
},
63+
"update_time": {
64+
Type: schema.TypeString,
65+
Computed: true,
66+
Description: "The time the package was last updated.",
67+
},
68+
},
69+
}
70+
}
71+
72+
func DataSourceArtifactRegistryPythonPackageRead(d *schema.ResourceData, meta interface{}) error {
73+
config := meta.(*transport_tpg.Config)
74+
userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent)
75+
if err != nil {
76+
return err
77+
}
78+
79+
project, err := tpgresource.GetProject(d, config)
80+
if err != nil {
81+
return err
82+
}
83+
84+
var res PythonPackage
85+
86+
packageName, version := parsePythonPackage(d.Get("package_name").(string))
87+
88+
if version != "" {
89+
// fetch package by version
90+
// https://cloud.google.com/artifact-registry/docs/reference/rest/v1/projects.locations.repositories.pythonPackages/get
91+
packageUrlSafe := url.QueryEscape(packageName)
92+
urlRequest, err := tpgresource.ReplaceVars(d, config, fmt.Sprintf("{{ArtifactRegistryBasePath}}projects/{{project}}/locations/{{location}}/repositories/{{repository_id}}/pythonPackages/%s:%s", packageUrlSafe, version))
93+
if err != nil {
94+
return fmt.Errorf("Error setting api endpoint")
95+
}
96+
97+
resGet, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{
98+
Config: config,
99+
Method: "GET",
100+
RawURL: urlRequest,
101+
UserAgent: userAgent,
102+
})
103+
if err != nil {
104+
return err
105+
}
106+
107+
res = convertPythonPackageResponseToStruct(resGet)
108+
} else {
109+
// fetch the list of packages, ordered by update time
110+
// https://cloud.google.com/artifact-registry/docs/reference/rest/v1/projects.locations.repositories.pythonPackages/list
111+
urlRequest, err := tpgresource.ReplaceVars(d, config, "{{ArtifactRegistryBasePath}}projects/{{project}}/locations/{{location}}/repositories/{{repository_id}}/pythonPackages")
112+
if err != nil {
113+
return fmt.Errorf("Error setting api endpoint")
114+
}
115+
116+
// to reduce the number of pages we need to fetch, we set the pageSize to 1000(max)
117+
urlRequest, err = transport_tpg.AddQueryParams(urlRequest, map[string]string{"pageSize": "1000"})
118+
if err != nil {
119+
return err
120+
}
121+
122+
res, err = retrieveAndFilterPythonPackages(d, config, urlRequest, userAgent, packageName, version)
123+
if err != nil {
124+
return err
125+
}
126+
}
127+
128+
// Set Terraform schema fields
129+
if err := d.Set("project", project); err != nil {
130+
return err
131+
}
132+
if err := d.Set("name", res.name); err != nil {
133+
return err
134+
}
135+
if err := d.Set("version", res.version); err != nil {
136+
return err
137+
}
138+
if err := d.Set("create_time", res.createTime); err != nil {
139+
return err
140+
}
141+
if err := d.Set("update_time", res.updateTime); err != nil {
142+
return err
143+
}
144+
145+
d.SetId(res.name)
146+
147+
return nil
148+
}
149+
150+
func parsePythonPackage(pkg string) (packageName string, version string) {
151+
splitByColon := strings.Split(pkg, ":")
152+
153+
if len(splitByColon) == 2 {
154+
packageName = splitByColon[0]
155+
version = splitByColon[1]
156+
} else {
157+
packageName = pkg
158+
}
159+
160+
return packageName, version
161+
}
162+
163+
func retrieveAndFilterPythonPackages(d *schema.ResourceData, config *transport_tpg.Config, urlRequest string, userAgent string, packageName string, version string) (PythonPackage, error) {
164+
// Paging through the list method until either:
165+
// if a version was provided, the matching package name and version pair
166+
// otherwise, return the first matching package name
167+
168+
var allPackages []PythonPackage
169+
170+
for {
171+
resListPythonPackages, token, err := retrieveListOfPythonPackages(config, urlRequest, userAgent)
172+
if err != nil {
173+
return PythonPackage{}, err
174+
}
175+
176+
for _, pkg := range resListPythonPackages {
177+
if strings.Contains(pkg.name, "/"+url.QueryEscape(packageName)+":") {
178+
allPackages = append(allPackages, pkg)
179+
}
180+
}
181+
182+
if token == "" {
183+
break
184+
}
185+
186+
urlRequest, err = transport_tpg.AddQueryParams(urlRequest, map[string]string{"pageToken": token})
187+
if err != nil {
188+
return PythonPackage{}, err
189+
}
190+
}
191+
192+
if len(allPackages) == 0 {
193+
return PythonPackage{}, fmt.Errorf("Requested Python package was not found.")
194+
}
195+
196+
// Client-side sort by updateTime descending (latest first)
197+
sort.Slice(allPackages, func(i, j int) bool {
198+
// Parse RFC3339 timestamps, fallback to string compare if parse fails
199+
ti, err1 := time.Parse(time.RFC3339, allPackages[i].updateTime)
200+
tj, err2 := time.Parse(time.RFC3339, allPackages[j].updateTime)
201+
if err1 == nil && err2 == nil {
202+
return ti.After(tj)
203+
}
204+
return allPackages[i].updateTime > allPackages[j].updateTime
205+
})
206+
207+
if version != "" {
208+
for _, pkg := range allPackages {
209+
if pkg.version == version {
210+
return pkg, nil
211+
}
212+
}
213+
return PythonPackage{}, fmt.Errorf("Requested version was not found.")
214+
}
215+
216+
// Return the latest package if no version specified
217+
return allPackages[0], nil
218+
}
219+
220+
func retrieveListOfPythonPackages(config *transport_tpg.Config, urlRequest string, userAgent string) ([]PythonPackage, string, error) {
221+
resList, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{
222+
Config: config,
223+
Method: "GET",
224+
RawURL: urlRequest,
225+
UserAgent: userAgent,
226+
})
227+
if err != nil {
228+
return make([]PythonPackage, 0), "", err
229+
}
230+
231+
if nextPageToken, ok := resList["nextPageToken"].(string); ok {
232+
return flattenPythonPackageDataSourceListResponse(resList), nextPageToken, nil
233+
} else {
234+
return flattenPythonPackageDataSourceListResponse(resList), "", nil
235+
}
236+
}
237+
238+
func flattenPythonPackageDataSourceListResponse(res map[string]interface{}) []PythonPackage {
239+
var pythonPackages []PythonPackage
240+
241+
resPythonPackages, _ := res["pythonPackages"].([]interface{})
242+
243+
for _, resPackage := range resPythonPackages {
244+
pkg, _ := resPackage.(map[string]interface{})
245+
pythonPackages = append(pythonPackages, convertPythonPackageResponseToStruct(pkg))
246+
}
247+
248+
return pythonPackages
249+
}
250+
251+
func convertPythonPackageResponseToStruct(res map[string]interface{}) PythonPackage {
252+
var pythonPackage PythonPackage
253+
254+
if name, ok := res["name"].(string); ok {
255+
pythonPackage.name = name
256+
}
257+
258+
if packageName, ok := res["packageName"].(string); ok {
259+
pythonPackage.packageName = packageName
260+
}
261+
262+
if version, ok := res["version"].(string); ok {
263+
pythonPackage.version = version
264+
}
265+
266+
if createTime, ok := res["createTime"].(string); ok {
267+
pythonPackage.createTime = createTime
268+
}
269+
270+
if updateTime, ok := res["updateTime"].(string); ok {
271+
pythonPackage.updateTime = updateTime
272+
}
273+
274+
return pythonPackage
275+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package artifactregistry_test
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
8+
"github.com/hashicorp/terraform-plugin-testing/terraform"
9+
"github.com/hashicorp/terraform-provider-google/google/acctest"
10+
)
11+
12+
func TestAccDataSourceArtifactRegistryPythonPackage_basic(t *testing.T) {
13+
acctest.SkipIfVcr(t)
14+
t.Parallel()
15+
16+
// At the moment there are no public Python packages available in Artifact Registry.
17+
// This test is skipped to avoid unnecessary failures.
18+
// As soon as there are public packages available, this test can be enabled by removing the skip and adjusting the configuration accordingly.
19+
t.Skip("No public Python packages available in Artifact Registry")
20+
21+
resourceName := "data.google_artifact_registry_python_package.test"
22+
23+
acctest.VcrTest(t, resource.TestCase{
24+
PreCheck: func() { acctest.AccTestPreCheck(t) },
25+
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
26+
Steps: []resource.TestStep{
27+
{
28+
Config: testAccDataSourceArtifactRegistryPythonPackageConfig,
29+
Check: resource.ComposeTestCheckFunc(
30+
resource.TestCheckResourceAttrSet(resourceName, "project"),
31+
resource.TestCheckResourceAttrSet(resourceName, "location"),
32+
resource.TestCheckResourceAttrSet(resourceName, "repository_id"),
33+
resource.TestCheckResourceAttrSet(resourceName, "package_name"),
34+
resource.TestCheckResourceAttrSet(resourceName, "name"),
35+
resource.TestCheckResourceAttrSet(resourceName, "version"),
36+
validatePythonPackageTimestamps(resourceName),
37+
),
38+
},
39+
},
40+
})
41+
}
42+
43+
const testAccDataSourceArtifactRegistryPythonPackageConfig = `
44+
data "google_artifact_registry_python_package" "test" {
45+
project = "example-project"
46+
location = "us"
47+
repository_id = "example-repo"
48+
package_name = "example-package"
49+
}
50+
`
51+
52+
func validatePythonPackageTimestamps(dataSourceName string) resource.TestCheckFunc {
53+
return func(s *terraform.State) error {
54+
res, ok := s.RootModule().Resources[dataSourceName]
55+
if !ok {
56+
return fmt.Errorf("can't find %s in state", dataSourceName)
57+
}
58+
59+
for _, attr := range []string{"create_time", "update_time"} {
60+
if ts, ok := res.Primary.Attributes[attr]; !ok || !isRFC3339(ts) {
61+
return fmt.Errorf("%s is not RFC3339: %s", attr, ts)
62+
}
63+
}
64+
65+
return nil
66+
}
67+
}

0 commit comments

Comments
 (0)