Skip to content

Commit b6c1670

Browse files
LucaPreteLuca Prete
andauthored
Adds google_apigee_developer_app resource and enhance google_apigee_api_product scopes to be order-insensitive (#15336)
Co-authored-by: Luca Prete <[email protected]>
1 parent 2013c83 commit b6c1670

File tree

9 files changed

+725
-1
lines changed

9 files changed

+725
-1
lines changed

mmv1/products/apigee/ApiProduct.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ properties:
154154
Comma-separated list of OAuth scopes that are validated at runtime. Apigee validates that the scopes in any access token presented match the scopes defined in the OAuth policy associated with the API product.
155155
item_type:
156156
type: String
157+
custom_flatten: 'templates/terraform/custom_flatten/apigee_api_product_scopes.go.tmpl'
157158

158159
- name: "quota"
159160
type: String
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
# Copyright 2025 Google Inc.
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
14+
---
15+
name: "DeveloperApp"
16+
description: |
17+
Creates an app associated with a developer.
18+
This API associates the developer app with the specified API product
19+
and auto-generates an API key for the app to use in calls to API proxies
20+
inside that API product.
21+
references:
22+
guides:
23+
"Creating a developer": "https://cloud.google.com/apigee/docs/api-platform/publish/creating-apps-surface-your-api"
24+
api: "https://cloud.google.com/apigee/docs/reference/apis/apigee/rest/v1/organizations.developers.apps"
25+
docs:
26+
base_url: "{{org_id}}/developers/{{developer_email}}/apps"
27+
self_link: "{{org_id}}/developers/{{developer_email}}/apps/{{name}}"
28+
update_mask: false
29+
update_verb: "PUT"
30+
import_format:
31+
- "{{org_id}}/developers/{{developer_email}}/apps/{{name}}"
32+
custom_code:
33+
decoder: "templates/terraform/decoders/apigee_developer_app.go.tmpl"
34+
custom_import: "templates/terraform/custom_import/apigee_developer_app.go.tmpl"
35+
examples:
36+
- name: "apigee_developer_app_basic"
37+
primary_resource_id: "apigee_developer_app"
38+
vars:
39+
api_product_name: "sample-api"
40+
developer_app_name: "sample-app"
41+
developer_email: "[email protected]"
42+
instance_name: "instance"
43+
exclude_test: true
44+
- name: "apigee_developer_app_basic_test"
45+
primary_resource_id: "apigee_developer_app"
46+
vars:
47+
api_product_name: "sample-api"
48+
developer_app_name: "sample-app"
49+
developer_email: "[email protected]"
50+
instance_name: "instance"
51+
project_name: "prj"
52+
test_env_vars:
53+
org_id: "ORG_ID"
54+
billing_account: "BILLING_ACCT"
55+
skip_vcr: true
56+
external_providers: ["time"]
57+
parameters:
58+
- name: "orgId"
59+
type: String
60+
description: |
61+
The Apigee Organization associated with the Apigee instance,
62+
in the format `organizations/{{org_name}}`.
63+
url_param_only: true
64+
required: true
65+
immutable: true
66+
- name: "developer_email"
67+
type: String
68+
description: |
69+
Email address of the developer.
70+
This value is used to uniquely identify the developer in Apigee hybrid.
71+
Note that the email address has to be in lowercase only.
72+
url_param_only: true
73+
required: true
74+
immutable: true
75+
properties:
76+
- name: "name"
77+
type: String
78+
description: |
79+
Name of the developer app.
80+
required: true
81+
- name: "appFamily"
82+
type: String
83+
description: |
84+
Developer app family.
85+
immutable: true
86+
default_from_api: true
87+
- name: "callbackUrl"
88+
type: String
89+
description: |
90+
Callback URL used by OAuth 2.0 authorization servers to communicate
91+
authorization codes back to developer apps.
92+
required: true
93+
- name: "keyExpiresIn"
94+
type: String
95+
description: |
96+
Expiration time, in milliseconds, for the consumer key that is generated
97+
for the developer app. If not set or left to the default value of -1,
98+
the API key never expires. The expiration time can't be updated after it is set.
99+
default_value: "-1"
100+
immutable: true
101+
- name: "apiProducts"
102+
type: Array
103+
description: |
104+
List of API products associated with the developer app.
105+
item_type:
106+
type: String
107+
is_set: true
108+
- name: "scopes"
109+
type: Array
110+
111+
description: |
112+
Scopes to apply to the developer app.
113+
The specified scopes must already exist for the API product that
114+
you associate with the developer app.
115+
item_type:
116+
type: String
117+
is_set: true
118+
- name: "developerId"
119+
type: String
120+
description: |
121+
ID of the developer.
122+
output: true
123+
- name: "status"
124+
type: String
125+
description: |
126+
Status of the credential. Valid values include approved or revoked.
127+
# This can be set but it needs to be updated through other APIs.
128+
immutable: true
129+
default_from_api: true
130+
- name: "attributes"
131+
type: Array
132+
description: |
133+
Developer attributes (name/value pairs). The custom attribute limit is 18.
134+
item_type:
135+
type: NestedObject
136+
properties:
137+
- name: "name"
138+
type: String
139+
description: |
140+
Key of the attribute
141+
- name: "value"
142+
type: String
143+
description: |
144+
Value of the attribute
145+
- name: "appId"
146+
type: String
147+
description: |
148+
ID of the developer app. This ID is not user specified but is
149+
automatically generated on app creation. appId is a UUID.
150+
output: true
151+
- name: "createdAt"
152+
type: String
153+
description: |
154+
Time at which the developer was created in milliseconds since epoch.
155+
output: true
156+
- name: "lastModifiedAt"
157+
type: String
158+
description: |
159+
Time at which the developer was last modified in milliseconds since epoch.
160+
output: true
161+
- name: "credentials"
162+
type: Array
163+
description: |
164+
Output only. Set of credentials for the developer app consisting of
165+
the consumer key/secret pairs associated with the API products.
166+
output: true
167+
item_type:
168+
type: NestedObject
169+
properties:
170+
- name: "consumerKey"
171+
type: String
172+
description: |
173+
Consumer key.
174+
output: true
175+
- name: "consumerSecret"
176+
type: String
177+
description: |
178+
Secret key.
179+
sensitive: true
180+
output: true
181+
- name: "expiresAt"
182+
type: String
183+
description: |
184+
Time the credential will expire in milliseconds since epoch.
185+
output: true
186+
- name: "issuedAt"
187+
type: String
188+
description: |
189+
Time the credential was issued in milliseconds since epoch.
190+
output: true
191+
- name: "status"
192+
type: String
193+
description: |
194+
Status of the credential. Valid values include approved or revoked.
195+
output: true
196+
- name: "scopes"
197+
type: Array
198+
description: |
199+
List of scopes to apply to the app.
200+
Specified scopes must already exist on the API product that
201+
you associate with the app.
202+
item_type:
203+
type: String
204+
output: true
205+
- name: "apiProducts"
206+
type: Array
207+
description: |
208+
List of API products this credential can be used for.
209+
output: true
210+
item_type:
211+
type: NestedObject
212+
properties:
213+
- name: "apiproduct"
214+
type: String
215+
description: |
216+
Name of the API product.
217+
output: true
218+
- name: "status"
219+
type: String
220+
description: |
221+
Status of the API product. Valid values are approved or revoked.
222+
output: true
223+
- name: "attributes"
224+
type: Array
225+
description: |
226+
Developer attributes (name/value pairs). The custom attribute limit is 18.
227+
output: true
228+
item_type:
229+
type: NestedObject
230+
properties:
231+
- name: "name"
232+
type: String
233+
description: |
234+
Key of the attribute
235+
output: true
236+
- name: "value"
237+
type: String
238+
description: |
239+
Value of the attribute
240+
output: true
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{{/*
2+
The license inside this block applies to this file
3+
Copyright 2025 Google Inc.
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/ -}}
13+
func flatten{{$.GetPrefix}}{{$.TitlelizeProperty}}(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} {
14+
rawConfigValue := d.Get("scopes")
15+
// Convert config value to []string
16+
configValue, err := tpgresource.InterfaceSliceToStringSlice(rawConfigValue)
17+
if err != nil {
18+
log.Printf("[ERROR] Failed to convert config value: %s", err)
19+
return v
20+
}
21+
sortedConfigValue := append([]string{}, configValue...)
22+
sort.Strings(sortedConfigValue)
23+
24+
// Convert v to []string
25+
apiValue, err := tpgresource.InterfaceSliceToStringSlice(v)
26+
if err != nil {
27+
log.Printf("[ERROR] Failed to convert API value: %s", err)
28+
return v
29+
}
30+
sortedApiValue := append([]string{}, apiValue...)
31+
sort.Strings(sortedApiValue)
32+
33+
if (slices.Equal(sortedApiValue, sortedConfigValue)) {
34+
return configValue
35+
}
36+
37+
return apiValue
38+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
config := meta.(*transport_tpg.Config)
2+
3+
importFormat := "(?P<org_id>organizations/[^/]+)/developers/(?P<developer_email>[^/]+)/apps/(?P<name>.+)"
4+
5+
if err := tpgresource.ParseImportId([]string{importFormat}, d, config); err != nil {
6+
return nil, err
7+
}
8+
9+
id, err := tpgresource.ReplaceVars(d, config, "{{ "{{" }}org_id{{ "}}" }}/developers/{{ "{{" }}developer_email{{ "}}" }}/apps/{{ "{{" }}name{{ "}}" }}")
10+
if err != nil {
11+
return nil, fmt.Errorf("Error constructing id: %s", err)
12+
}
13+
d.SetId(id)
14+
15+
return []*schema.ResourceData{d}, nil
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{{/*
2+
The license inside this block applies to this file
3+
Copyright 2025 Google Inc.
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/ -}}
13+
if obj, ok := res["credentials"]; ok {
14+
if credList, ok := obj.([]interface{}); ok && len(credList) > 0 {
15+
if cred, ok := credList[0].(map[string]interface{}); ok {
16+
// Decode expiresAt
17+
res["keyExpiresIn"] = cred["expiresAt"]
18+
19+
// Decode scopes
20+
res["scopes"] = cred["scopes"]
21+
22+
// Decode api_products
23+
if apiProductsObj, productsOk := cred["apiProducts"]; productsOk {
24+
if apiProductList, listOk := apiProductsObj.([]interface{}); listOk {
25+
var flattenedProducts []interface{}
26+
for _, productObj := range apiProductList {
27+
if productMap, mapOk := productObj.(map[string]interface{}); mapOk {
28+
if productName, nameOk := productMap["apiproduct"].(string); nameOk {
29+
flattenedProducts = append(flattenedProducts, productName)
30+
}
31+
}
32+
}
33+
res["apiProducts"] = flattenedProducts
34+
}
35+
}
36+
37+
delete(res, "credentials")
38+
} else {
39+
return nil, fmt.Errorf("Unable to decode the first element of the credentials array.")
40+
}
41+
} else {
42+
return nil, fmt.Errorf("Unable to decode credentials block from API response, expected a non-empty array.")
43+
}
44+
}
45+
return res, nil

mmv1/templates/terraform/examples/apigee_api_product_with_attributes.tf.tmpl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,11 @@ resource "google_apigee_api_product" "full_api_product" {
4747
quota_counter_scope = "PROXY"
4848

4949
environments = ["dev", "hom"]
50+
51+
# Set them in reverse order to test set
5052
scopes = [
51-
"read:weather",
5253
"write:reports"
54+
"read:weather",
5355
]
5456

5557
attributes {

0 commit comments

Comments
 (0)