Skip to content

Commit 05f5194

Browse files
authored
Merge pull request hashicorp#212 from hashicorp/port-http-import-examples
ported http import policies to 3gen
2 parents 8714ede + 5c29b8d commit 05f5194

File tree

20 files changed

+949
-1
lines changed

20 files changed

+949
-1
lines changed

governance/second-generation/cloud-agnostic/http-examples/check-external-http-api.sentinel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import "json"
2121

2222
##### Functions #####
2323

24-
# Validate that the proposed monthly cost is less than the limit
24+
# Validate that the external system returns yes or maybe
2525
check_external_approval_system = func() {
2626
req = http.request("https://yesno.wtf/api")
2727
res = json.unmarshal(http.get(req).body)

governance/second-generation/cloud-agnostic/http-examples/sentinel.hcl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,7 @@ policy "check-external-http-api" {
55
policy "use-latest-module-versions" {
66
enforcement_level = "advisory"
77
}
8+
9+
policy "asteroids" {
10+
enforcement_level = "advisory"
11+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Sentinel HTTP Import and Parameters Examples
2+
This directory contains examples of using the [HTTP import](https://docs.hashicorp.com/sentinel/imports/http) and [policy parameters](https://docs.hashicorp.com/sentinel/language/parameters) that were added in the Sentinel 0.13.0 runtime. Policy parameters allow you to specify API credentials without storing them in your policies which would be undesirable since policies are stored in VCS repositories.
3+
4+
Be sure to use Sentinel 0.15.2 or higher with these policies.
5+
6+
These policies are essentially the same as the second-generation versions in this [directory](../../../second-generation/cloud-agnostic/http-examples), but the [use-latest-module-versions.sentinel](./use-latest-module-versions.sentinel) in this directory uses the [tfconfig/v2](https://www.terraform.io/docs/cloud/sentinel/import/tfconfig-v2.html) import instead of the older [tfconfig](https://www.terraform.io/docs/cloud/sentinel/import/tfconfig.html) import. It uses that import indirectly by calling some functions from the [tfconfig-functions](../../common-functions/tfconfig-functions) Sentinel module.
7+
8+
## Policies
9+
There are currently three example policies in this directory:
10+
* [check-external-http-api.sentinel](./check-external-http-api.sentinel)
11+
* [use-latest-module-versions.sentinel](./use-latest-module-versions.sentinel)
12+
* [asteroids.sentinel](./asteroids.sentinel)
13+
14+
The first policy simply uses the HTTP import to call an external API, https://yesno.wtf/api that randomly returns "yes" or "no" (but sometimes returns "maybe"). It also uses the recently added [case statement](https://docs.hashicorp.com/sentinel/language/spec/#case-statements) that provides a selection control mechanism to conditionally execute different logic based on the value of an argument.
15+
16+
You can test the first policy from this directory (after forking or cloning the repository and [installing the Sentinel CLI](https://docs.hashicorp.com/sentinel/intro/getting-started/install/)) with this command:
17+
```
18+
sentinel test -run=check -verbose
19+
```
20+
21+
The second policy uses the HTTP import to call the Terraform Registry [List Modules API](https://www.terraform.io/docs/registry/api.html#list-modules) against a Terraform Cloud or Terraform Enterprise server in order to determine the most recent version of each module in the [Private Module Registry](https://www.terraform.io/docs/cloud/registry/index.html) (PMR) of an organization on that server or in the [public Terraform registry](https://registry.terraform.io). This policy also uses parameters as described below.
22+
23+
The third policy uses the HTTP import to call a [NASA API](https://api.nasa.gov/) that retrieves a list of Near Earth Objects and warns if any of them are too close for comfort. This is based on an example from this HashiCorp [blog](https://www.hashicorp.com/blog/announcing-business-aware-policies-for-terraform-cloud-and-enterprise/) that announced the HTTP import and "Business-aware Policies". This policy also uses parameters as described below.
24+
25+
## Use of Parameters in use-latest-module-versions.sentinel
26+
The [use-latest-module-versions.sentinel](./use-latest-module-versions.sentinel) policy uses four parameters:
27+
* `public_registry` indicates whether the public Terraform registry is being used. This is `false` by default, but could be set to `true`.
28+
* `address` gives the address of the Terraform Cloud or Terraform Enterprise server. It defaults to `app.terraform.io` which is the address of the multi-tenant Terraform Cloud server that HashiCorp runs. You must specify a value for this if using a Terraform Enterprise server.
29+
* `organization` gives the name of an [organization](https://www.terraform.io/docs/cloud/users-teams-organizations/organizations.html) on the Terraform Cloud or Terraform Enterprise server specified by `address`. You must always specify a valid organization.
30+
* `token` gives a valid Terraform Cloud API token which can be a user, team, or organization token. See the [API tokens](https://www.terraform.io/docs/cloud/users-teams-organizations/api-tokens.html) document for more information.
31+
32+
## Use of Parameters in asteroids.sentinel
33+
The [asteroids.sentinel](./asteroids.sentinel) policy uses two parameters:
34+
* `api_token` must be set to a NASA API token. (See below.)
35+
* `danger_distance` specifies a distance in miles such that any Near Earth Object that would come within that many miles of Earth will generate a Sentinel violation.
36+
37+
Before you can test asteroids.sentinel with the provided test cases and mocks, you must obtain a NASA API token from https://api.nasa.gov and then add it to the `api_token` field of the pass.json and fail.json test cases under the test/asteroids directory.
38+
39+
## Using Parameters with the Sentinel CLI
40+
While parameters can currently be set with environment variables when using the `sentinel apply` command, they cannot be set with environment variables when using the `sentinel test` command.
41+
42+
Consequently, you **cannot** test the use-latest-module-versions.sentinel policy with the `sentinel test` command using the provided test cases and mocks since you will not have a token allowed to call the API against the specified Cloud-Operations organization.
43+
44+
You could test the policy with the `sentinel test` command if you edited the mocks to reference modules contained in a PMR in an organization on your own TFC or TFE organization or contained in the public registry and added your own valid API token to the test cases.
45+
46+
Since many readers won't have modules in their own TFC/TFE organization, we have provided a [sentinel.json](./sentinel.json) configuration file and an additional mock file [mocks/mock-tfconfig-fail.sentinel](./mocks/mock-tfconfig-fail.sentinel) that references modules from the [public Terraform registry](https://registry.terraform.io). These allow you to run the `sentinel apply` command to use the use-latest-module-versions.sentinel policy.
47+
48+
Specifically, you can run this command to test that the versions of the Azure modules from the public module registry are the latest:
49+
```
50+
sentinel apply use-latest-module-versions.sentinel -trace
51+
```
52+
You do not need a token when talking to the public registry, so the sentinel.json file sets `token` to an empty string.
53+
54+
The policy should fail since the mock does not use the most recent versions of the two modules. If you would like to see the policy pass, change the versions of the modules in mocks/mock-tfconfig-fail.sentinel to the most recent versions listed under https://registry.terraform.io/modules/Azure/network/azurerm and https://registry.terraform.io/modules/Azure/compute/azurerm. Currently, those are "3.0.1" and "3.2.0" respectively.
55+
56+
Note that the `sentinel test` and `sentinel apply` commands for testing/applying the use-latest-module-versions.sentinel policy **really** are making HTTP calls to the API endpoints to retrieve the list of matching modules in the registries. However, the mocks simulate which modules would actually be used by Terraform code.
57+
58+
You should **not** edit sentinel.json unless you also edit mocks/mock-tfconfig-fail.sentinel to reference actual modules in the registry and organization that sentinel.json refers to.
59+
60+
## Using Parameters with Terraform Cloud/Enterprise
61+
If you wish to use the [use-latest-module-versions.sentinel](./use-latest-module-versions.sentinel) policy on a Terraform Cloud (TFC) or Terraform Enterprise (TFE) server, you need to specify values for the `organization` and `token` parameters when registering the policy set that contains this policy. Only do this if you have actually created some modules in the Private Module Registry (PMR) in an organization on your server and have Terraform code that uses them.
62+
63+
You can do this as follows:
64+
1. Copy the files [check-external-http-api.sentinel](./check-external-http-api.sentinel), [use-latest-module-versions.sentinel](./use-latest-module-versions.sentinel), and [sentinel.hcl](./sentinel.hcl) into a VCS repository. (Don't copy the file sentinel.json which is only for use with the Sentinel CLI.)
65+
1. Optionally edit the copy of sentinel.hcl to set the enforcement_level for the use-latest-module-versions policy to `soft-mandatory`.
66+
1. Commit the files to your VCS repository.
67+
1. Instead of doing the above 3 steps, you could fork the [test-http-policies-and-parameters](https://github.com/rberlind/test-http-policies-and-parameters) repository and use that fork.
68+
1. [Register a new policy set](https://www.terraform.io/docs/cloud/sentinel/manage-policies.html#managing-policy-sets) on your Terraform Cloud or Terraform Enterprise server.
69+
1. Edit the registered policy sets to specify values for the `organization` and `token` parameters making sure you pick an organization that actually has some modules in its PMR and that the token you give is a valid API token with permission in that organization. (You cannot specify parameters until after creating the policy set.) Parameters are added at the bottom of the Policy Set screen.
70+
1. Be sure to mark your `token` parameter as sensitive so that nobody else can see it in the Terraform Cloud UI.
71+
1. If using a Terraform Enterprise server, also specify a value for the `address` parameter, using a value like "tfe.example.com".
72+
1. Save the policy set.
73+
1. Add a workspace to the policy set that uses Terraform code that references modules in the PMR in the organization you specified.
74+
1. Queue a plan against that workspace in the Terraform Cloud UI.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# no-hazardous-asteroids-today.sentinel enforces that no
2+
# "potentially hazardous" asteroids are approaching their
3+
# closest point to Earth within 1,000,000 miles today. Because if
4+
# there's a potentially hazardous asteroid reaching within a
5+
# million miles o' here, we're way too nervous and distracted to
6+
# be changing our infrastructure right now!
7+
import "http"
8+
import "json"
9+
import "strings"
10+
import "time"
11+
12+
param api_token
13+
param danger_distance
14+
15+
print("danger distance set to:", danger_distance)
16+
17+
year = string(time.now.year)
18+
month = string(time.now.month)
19+
if length(month) is 1 {
20+
month = "0" + month
21+
}
22+
day = string(time.now.day)
23+
if length(day) is 1 {
24+
day = "0" + day
25+
}
26+
today = strings.join([year, month, day], "-")
27+
base_url = "https://api.nasa.gov/neo/rest/v1/feed?"
28+
start_query = "start_date=" + today
29+
api_query = "&api_key=" + api_token
30+
full_url = base_url + start_query + api_query
31+
32+
no_close_hazardous_asteroids = func(asteroids, danger_distance) {
33+
is_safe = true
34+
for asteroids else [] as asteroid {
35+
hazardous = asteroid["is_potentially_hazardous_asteroid"]
36+
approach_data = asteroid["close_approach_data"][0]
37+
distance = approach_data["miss_distance"]["miles"]
38+
if float(distance) < danger_distance {
39+
diameter = asteroid["estimated_diameter"]["feet"]
40+
max_diameter = diameter["estimated_diameter_max"]
41+
mph = approach_data["relative_velocity"]["miles_per_hour"]
42+
43+
warning_message = [
44+
"\n😱😱😱\n",
45+
"The asteroid '" + string(asteroid["name"]) + "'",
46+
"is estimated to be up to " + string(max_diameter),
47+
"feet in diameter!\n",
48+
"And it's traveling " + string(mph) + " miles per hour\n",
49+
"and will reach minimum distance from Earth of " + distance + " miles!!!\n",
50+
"AHHHHHHH!!!!!!\n",
51+
]
52+
53+
print(strings.join(warning_message, " "))
54+
is_safe = false
55+
}
56+
}
57+
return is_safe
58+
}
59+
60+
resp = http.get(full_url)
61+
near_earth_objects = json.unmarshal(resp.body)["near_earth_objects"][today]
62+
#print("near earth objects:", json.unmarshal(resp.body))
63+
64+
no_close_asteroids = no_close_hazardous_asteroids(near_earth_objects, danger_distance)
65+
main = rule {
66+
no_close_asteroids
67+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# This policy uses the Sentinel HTTP import to call an external API,
2+
# https://yesno.wtf/api that randomly returns "yes" or "no"
3+
# This simulates what a policy might do to check an external system
4+
# that has a JSON-based API in order to confirm that the run is
5+
# allowed to do an apply.
6+
7+
# For example, some customers require tickets to be opened and approved
8+
# before an apply can be done. The HTTP import could be used to check
9+
# those types of systems.
10+
11+
# It also uses the Sentinel case statement
12+
13+
# Note that the single associated test.json test case will pass sometimes and
14+
# fail the other times depending on the value returned by the API. To see the
15+
# answer that was returned, run `sentinel test -run=check -verbose`
16+
17+
18+
##### Imports #####
19+
import "http"
20+
import "json"
21+
22+
##### Functions #####
23+
24+
# Validate that the external system returns yes or maybe
25+
check_external_approval_system = func() {
26+
req = http.request("https://yesno.wtf/api")
27+
res = json.unmarshal(http.get(req).body)
28+
answer = res.answer
29+
print("answer:", answer)
30+
31+
case answer {
32+
# https://yesno.wtf/api returns "maybe" every 10,000th time
33+
when "yes", "maybe":
34+
return true
35+
when "no":
36+
return false
37+
else:
38+
return false
39+
}
40+
41+
}
42+
43+
##### Rules #####
44+
45+
# Call the validation function
46+
approved = check_external_approval_system()
47+
48+
# Main rule
49+
main = rule {
50+
approved
51+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import "strings"
2+
3+
module_calls = {
4+
"network": {
5+
"config": {
6+
"allow_ssh_traffic": {
7+
"constant_value": true,
8+
},
9+
"location": {
10+
"references": [
11+
"var.location",
12+
],
13+
},
14+
"resource_group_name": {
15+
"references": [
16+
"var.windows_dns_prefix",
17+
],
18+
},
19+
},
20+
"count": {},
21+
"for_each": {},
22+
"module_address": "",
23+
"name": "network",
24+
"source": "Azure/network/azurerm",
25+
"version_constraint": "1.1.1",
26+
},
27+
"windowsserver": {
28+
"config": {
29+
"admin_password": {
30+
"references": [
31+
"var.admin_password",
32+
],
33+
},
34+
"location": {
35+
"references": [
36+
"var.location",
37+
],
38+
},
39+
"public_ip_dns": {
40+
"references": [
41+
"var.windows_dns_prefix",
42+
],
43+
},
44+
"resource_group_name": {
45+
"references": [
46+
"var.windows_dns_prefix",
47+
],
48+
},
49+
"storage_account_type": {
50+
"references": [
51+
"var.storage_account_type",
52+
],
53+
},
54+
"vm_hostname": {
55+
"constant_value": "demohost",
56+
},
57+
"vm_os_simple": {
58+
"constant_value": "WindowsServer",
59+
},
60+
"vm_size": {
61+
"references": [
62+
"var.vm_size",
63+
],
64+
},
65+
"vnet_subnet_id": {
66+
"references": [
67+
"module.network.vnet_subnets",
68+
],
69+
},
70+
},
71+
"count": {},
72+
"for_each": {},
73+
"module_address": "",
74+
"name": "windowsserver",
75+
"source": "Azure/compute/azurerm",
76+
"version_constraint": "1.1.7",
77+
},
78+
}
79+
80+
strip_index = func(addr) {
81+
s = strings.split(addr, ".")
82+
for s as i, v {
83+
s[i] = strings.split(v, "[")[0]
84+
}
85+
86+
return strings.join(s, ".")
87+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
policy "check-external-http-api" {
2+
enforcement_level = "advisory"
3+
}
4+
5+
policy "use-latest-module-versions" {
6+
enforcement_level = "advisory"
7+
}
8+
9+
policy "asteroids" {
10+
enforcement_level = "advisory"
11+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"param": {
3+
"public_registry": true,
4+
"address": "registry.terraform.io",
5+
"organization": "Azure",
6+
"token": ""
7+
},
8+
"modules": {
9+
"tfconfig-functions": {
10+
"path": "../../common-functions/tfconfig-functions/tfconfig-functions.sentinel"
11+
}
12+
},
13+
"mock": {
14+
"tfconfig/v2": "mocks/mock-tfconfig-fail.sentinel"
15+
}
16+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"param": {
3+
"api_token": "",
4+
"danger_distance": 10000000
5+
},
6+
"test": {
7+
"main": false
8+
}
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"param": {
3+
"api_token": "",
4+
"danger_distance": 200000
5+
},
6+
"test": {
7+
"main": true
8+
}
9+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"test": {
3+
"main": true
4+
}
5+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"param": {
3+
"organization": "Cloud-Operations",
4+
"token": ""
5+
},
6+
"modules": {
7+
"tfconfig-functions": {
8+
"path": "../../../../common-functions/tfconfig-functions/tfconfig-functions.sentinel"
9+
}
10+
},
11+
"mock": {
12+
"tfconfig/v2": "mock-tfconfig-fail.sentinel"
13+
},
14+
"test": {
15+
"main": false
16+
}
17+
}

0 commit comments

Comments
 (0)