diff --git a/.changelog/3564.txt b/.changelog/3564.txt new file mode 100644 index 00000000000..b94ac140929 --- /dev/null +++ b/.changelog/3564.txt @@ -0,0 +1,6 @@ +```release-note:enhancement +container_analysis: Added top-level generic note fields to `google_container_analysis_note` +``` +```release-note:new-resource +`google_container_analysis_occurence` +``` diff --git a/google/bootstrap_utils_test.go b/google/bootstrap_utils_test.go index 1f66cdfe23a..267c8a1f3bc 100644 --- a/google/bootstrap_utils_test.go +++ b/google/bootstrap_utils_test.go @@ -51,10 +51,12 @@ func BootstrapKMSKeyWithPurpose(t *testing.T, purpose string) bootstrappedKMS { * a KMS key. **/ func BootstrapKMSKeyWithPurposeInLocation(t *testing.T, purpose, locationID string) bootstrappedKMS { - if v := os.Getenv("TF_ACC"); v == "" { - t.Skip("Acceptance tests and bootstrapping skipped unless env 'TF_ACC' set") + return BootstrapKMSKeyWithPurposeInLocationAndName(t, purpose, locationID, SharedCryptoKey[purpose]) +} - // If not running acceptance tests, return an empty object +func BootstrapKMSKeyWithPurposeInLocationAndName(t *testing.T, purpose, locationID, keyShortName string) bootstrappedKMS { + config := BootstrapConfig(t) + if config == nil { return bootstrappedKMS{ &cloudkms.KeyRing{}, &cloudkms.CryptoKey{}, @@ -65,20 +67,7 @@ func BootstrapKMSKeyWithPurposeInLocation(t *testing.T, purpose, locationID stri keyRingParent := fmt.Sprintf("projects/%s/locations/%s", projectID, locationID) keyRingName := fmt.Sprintf("%s/keyRings/%s", keyRingParent, SharedKeyRing) keyParent := fmt.Sprintf("projects/%s/locations/%s/keyRings/%s", projectID, locationID, SharedKeyRing) - keyName := fmt.Sprintf("%s/cryptoKeys/%s", keyParent, SharedCryptoKey[purpose]) - - config := &Config{ - Credentials: getTestCredsFromEnv(), - Project: getTestProjectFromEnv(), - Region: getTestRegionFromEnv(), - Zone: getTestZoneFromEnv(), - } - - ConfigureBasePaths(config) - - if err := config.LoadAndValidate(context.Background()); err != nil { - t.Errorf("Unable to bootstrap KMS key: %s", err) - } + keyName := fmt.Sprintf("%s/cryptoKeys/%s", keyParent, keyShortName) // Get or Create the hard coded shared keyring for testing kmsClient := config.clientKms @@ -118,7 +107,7 @@ func BootstrapKMSKeyWithPurposeInLocation(t *testing.T, purpose, locationID stri } cryptoKey, err = kmsClient.Projects.Locations.KeyRings.CryptoKeys.Create(keyParent, &newKey). - CryptoKeyId(SharedCryptoKey[purpose]).Do() + CryptoKeyId(keyShortName).Do() if err != nil { t.Errorf("Unable to bootstrap KMS key. Cannot create new CryptoKey: %s", err) } @@ -202,24 +191,11 @@ func impersonationServiceAccountPermissions(config *Config, sa *iam.ServiceAccou } func BootstrapServiceAccount(t *testing.T, project, testRunner string) string { - if v := os.Getenv("TF_ACC"); v == "" { - t.Skip("Acceptance tests and bootstrapping skipped unless env 'TF_ACC' set") + config := BootstrapConfig(t) + if config == nil { return "" } - config := &Config{ - Credentials: getTestCredsFromEnv(), - Project: getTestProjectFromEnv(), - Region: getTestRegionFromEnv(), - Zone: getTestZoneFromEnv(), - } - - ConfigureBasePaths(config) - - if err := config.LoadAndValidate(context.Background()); err != nil { - t.Fatalf("Bootstrapping failed. Unable to load test config: %s", err) - } - sa, err := getOrCreateServiceAccount(config, project) if err != nil { t.Fatalf("Bootstrapping failed. Cannot retrieve service account, %s", err) @@ -244,23 +220,12 @@ const SharedTestNetworkPrefix = "tf-bootstrap-net-" // testId specifies the test/suite for which a shared network is used/initialized. // Returns the name of an network, creating it if hasn't been created in the test projcet. func BootstrapSharedTestNetwork(t *testing.T, testId string) string { - if v := os.Getenv("TF_ACC"); v == "" { - t.Skip("Acceptance tests and bootstrapping skipped unless env 'TF_ACC' set") - // If not running acceptance tests, return an empty string - return "" - } - project := getTestProjectFromEnv() networkName := SharedTestNetworkPrefix + testId - config := &Config{ - Credentials: getTestCredsFromEnv(), - Project: project, - Region: getTestRegionFromEnv(), - Zone: getTestZoneFromEnv(), - } - ConfigureBasePaths(config) - if err := config.LoadAndValidate(context.Background()); err != nil { - t.Errorf("Unable to bootstrap network: %s", err) + + config := BootstrapConfig(t) + if config == nil { + return "" } log.Printf("[DEBUG] Getting shared test network %q", networkName) @@ -298,24 +263,12 @@ func BootstrapSharedTestNetwork(t *testing.T, testId string) string { var SharedServicePerimeterProjectPrefix = "tf-bootstrap-sp-" func BootstrapServicePerimeterProjects(t *testing.T, desiredProjects int) []*cloudresourcemanager.Project { - if v := os.Getenv("TF_ACC"); v == "" { - t.Skip("Acceptance tests and bootstrapping skipped unless env 'TF_ACC' set") + config := BootstrapConfig(t) + if config == nil { return nil } org := getTestOrgFromEnv(t) - config := &Config{ - Credentials: getTestCredsFromEnv(), - Project: getTestProjectFromEnv(), - Region: getTestRegionFromEnv(), - Zone: getTestZoneFromEnv(), - } - - ConfigureBasePaths(config) - - if err := config.LoadAndValidate(context.Background()); err != nil { - t.Fatalf("Bootstrapping failed. Unable to load test config: %s", err) - } // The filter endpoint works differently if you provide both the parent id and parent type, and // doesn't seem to allow for prefix matching. Don't change this to include the parent type unless @@ -361,3 +314,24 @@ func BootstrapServicePerimeterProjects(t *testing.T, desiredProjects int) []*clo return projects } + +func BootstrapConfig(t *testing.T) *Config { + if v := os.Getenv("TF_ACC"); v == "" { + t.Skip("Acceptance tests and bootstrapping skipped unless env 'TF_ACC' set") + return nil + } + + config := &Config{ + Credentials: getTestCredsFromEnv(), + Project: getTestProjectFromEnv(), + Region: getTestRegionFromEnv(), + Zone: getTestZoneFromEnv(), + } + + ConfigureBasePaths(config) + + if err := config.LoadAndValidate(context.Background()); err != nil { + t.Fatalf("Bootstrapping failed. Unable to load test config: %s", err) + } + return config +} diff --git a/google/provider.go b/google/provider.go index ee90d54acd1..6b138e9326c 100644 --- a/google/provider.go +++ b/google/provider.go @@ -563,9 +563,9 @@ func Provider() terraform.ResourceProvider { return provider } -// Generated resources: 132 +// Generated resources: 133 // Generated IAM resources: 57 -// Total generated resources: 189 +// Total generated resources: 190 func ResourceMap() map[string]*schema.Resource { resourceMap, _ := ResourceMapWithErrors() return resourceMap @@ -666,6 +666,7 @@ func ResourceMapWithErrors() (map[string]*schema.Resource, error) { "google_compute_url_map": resourceComputeUrlMap(), "google_compute_vpn_tunnel": resourceComputeVpnTunnel(), "google_container_analysis_note": resourceContainerAnalysisNote(), + "google_container_analysis_occurrence": resourceContainerAnalysisOccurrence(), "google_data_catalog_entry_group": resourceDataCatalogEntryGroup(), "google_data_catalog_entry_group_iam_binding": ResourceIamBinding(DataCatalogEntryGroupIamSchema, DataCatalogEntryGroupIamUpdaterProducer, DataCatalogEntryGroupIdParseFunc), "google_data_catalog_entry_group_iam_member": ResourceIamMember(DataCatalogEntryGroupIamSchema, DataCatalogEntryGroupIamUpdaterProducer, DataCatalogEntryGroupIdParseFunc), diff --git a/google/resource_container_analysis_note.go b/google/resource_container_analysis_note.go index bf3c6dcaf0d..dce62ce39a0 100644 --- a/google/resource_container_analysis_note.go +++ b/google/resource_container_analysis_note.go @@ -89,6 +89,52 @@ example "qa".`, ForceNew: true, Description: `The name of the note.`, }, + "expiration_time": { + Type: schema.TypeString, + Optional: true, + Description: `Time of expiration for this note. Leave empty if note does not expire.`, + }, + "long_description": { + Type: schema.TypeString, + Optional: true, + Description: `A detailed description of the note`, + }, + "related_note_names": { + Type: schema.TypeSet, + Optional: true, + Description: `Names of other notes related to this note.`, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Set: schema.HashString, + }, + "related_url": { + Type: schema.TypeSet, + Optional: true, + Description: `URLs associated with this note and related metadata.`, + Elem: containeranalysisNoteRelatedUrlSchema(), + // Default schema.HashSchema is used. + }, + "short_description": { + Type: schema.TypeString, + Optional: true, + Description: `A one sentence description of the note.`, + }, + "create_time": { + Type: schema.TypeString, + Computed: true, + Description: `The time this note was created.`, + }, + "kind": { + Type: schema.TypeString, + Computed: true, + Description: `The type of analysis this note describes`, + }, + "update_time": { + Type: schema.TypeString, + Computed: true, + Description: `The time this note was last updated.`, + }, "project": { Type: schema.TypeString, Optional: true, @@ -99,6 +145,23 @@ example "qa".`, } } +func containeranalysisNoteRelatedUrlSchema() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "url": { + Type: schema.TypeString, + Required: true, + Description: `Specific URL associated with the resource.`, + }, + "label": { + Type: schema.TypeString, + Optional: true, + Description: `Label to describe usage of the URL`, + }, + }, + } +} + func resourceContainerAnalysisNoteCreate(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) @@ -109,6 +172,36 @@ func resourceContainerAnalysisNoteCreate(d *schema.ResourceData, meta interface{ } else if v, ok := d.GetOkExists("name"); !isEmptyValue(reflect.ValueOf(nameProp)) && (ok || !reflect.DeepEqual(v, nameProp)) { obj["name"] = nameProp } + shortDescriptionProp, err := expandContainerAnalysisNoteShortDescription(d.Get("short_description"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("short_description"); !isEmptyValue(reflect.ValueOf(shortDescriptionProp)) && (ok || !reflect.DeepEqual(v, shortDescriptionProp)) { + obj["shortDescription"] = shortDescriptionProp + } + longDescriptionProp, err := expandContainerAnalysisNoteLongDescription(d.Get("long_description"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("long_description"); !isEmptyValue(reflect.ValueOf(longDescriptionProp)) && (ok || !reflect.DeepEqual(v, longDescriptionProp)) { + obj["longDescription"] = longDescriptionProp + } + relatedUrlProp, err := expandContainerAnalysisNoteRelatedUrl(d.Get("related_url"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("related_url"); !isEmptyValue(reflect.ValueOf(relatedUrlProp)) && (ok || !reflect.DeepEqual(v, relatedUrlProp)) { + obj["relatedUrl"] = relatedUrlProp + } + expirationTimeProp, err := expandContainerAnalysisNoteExpirationTime(d.Get("expiration_time"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("expiration_time"); !isEmptyValue(reflect.ValueOf(expirationTimeProp)) && (ok || !reflect.DeepEqual(v, expirationTimeProp)) { + obj["expirationTime"] = expirationTimeProp + } + relatedNoteNamesProp, err := expandContainerAnalysisNoteRelatedNoteNames(d.Get("related_note_names"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("related_note_names"); !isEmptyValue(reflect.ValueOf(relatedNoteNamesProp)) && (ok || !reflect.DeepEqual(v, relatedNoteNamesProp)) { + obj["relatedNoteNames"] = relatedNoteNamesProp + } attestationAuthorityProp, err := expandContainerAnalysisNoteAttestationAuthority(d.Get("attestation_authority"), d, config) if err != nil { return err @@ -121,6 +214,13 @@ func resourceContainerAnalysisNoteCreate(d *schema.ResourceData, meta interface{ return err } + lockName, err := replaceVars(d, config, "projects/{{project}}/notes/{{name}}") + if err != nil { + return err + } + mutexKV.Lock(lockName) + defer mutexKV.Unlock(lockName) + url, err := replaceVars(d, config, "{{ContainerAnalysisBasePath}}projects/{{project}}/notes?noteId={{name}}") if err != nil { return err @@ -184,6 +284,30 @@ func resourceContainerAnalysisNoteRead(d *schema.ResourceData, meta interface{}) if err := d.Set("name", flattenContainerAnalysisNoteName(res["name"], d, config)); err != nil { return fmt.Errorf("Error reading Note: %s", err) } + if err := d.Set("short_description", flattenContainerAnalysisNoteShortDescription(res["shortDescription"], d, config)); err != nil { + return fmt.Errorf("Error reading Note: %s", err) + } + if err := d.Set("long_description", flattenContainerAnalysisNoteLongDescription(res["longDescription"], d, config)); err != nil { + return fmt.Errorf("Error reading Note: %s", err) + } + if err := d.Set("kind", flattenContainerAnalysisNoteKind(res["kind"], d, config)); err != nil { + return fmt.Errorf("Error reading Note: %s", err) + } + if err := d.Set("related_url", flattenContainerAnalysisNoteRelatedUrl(res["relatedUrl"], d, config)); err != nil { + return fmt.Errorf("Error reading Note: %s", err) + } + if err := d.Set("expiration_time", flattenContainerAnalysisNoteExpirationTime(res["expirationTime"], d, config)); err != nil { + return fmt.Errorf("Error reading Note: %s", err) + } + if err := d.Set("create_time", flattenContainerAnalysisNoteCreateTime(res["createTime"], d, config)); err != nil { + return fmt.Errorf("Error reading Note: %s", err) + } + if err := d.Set("update_time", flattenContainerAnalysisNoteUpdateTime(res["updateTime"], d, config)); err != nil { + return fmt.Errorf("Error reading Note: %s", err) + } + if err := d.Set("related_note_names", flattenContainerAnalysisNoteRelatedNoteNames(res["relatedNoteNames"], d, config)); err != nil { + return fmt.Errorf("Error reading Note: %s", err) + } if err := d.Set("attestation_authority", flattenContainerAnalysisNoteAttestationAuthority(res["attestationAuthority"], d, config)); err != nil { return fmt.Errorf("Error reading Note: %s", err) } @@ -200,6 +324,36 @@ func resourceContainerAnalysisNoteUpdate(d *schema.ResourceData, meta interface{ } obj := make(map[string]interface{}) + shortDescriptionProp, err := expandContainerAnalysisNoteShortDescription(d.Get("short_description"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("short_description"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, shortDescriptionProp)) { + obj["shortDescription"] = shortDescriptionProp + } + longDescriptionProp, err := expandContainerAnalysisNoteLongDescription(d.Get("long_description"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("long_description"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, longDescriptionProp)) { + obj["longDescription"] = longDescriptionProp + } + relatedUrlProp, err := expandContainerAnalysisNoteRelatedUrl(d.Get("related_url"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("related_url"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, relatedUrlProp)) { + obj["relatedUrl"] = relatedUrlProp + } + expirationTimeProp, err := expandContainerAnalysisNoteExpirationTime(d.Get("expiration_time"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("expiration_time"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, expirationTimeProp)) { + obj["expirationTime"] = expirationTimeProp + } + relatedNoteNamesProp, err := expandContainerAnalysisNoteRelatedNoteNames(d.Get("related_note_names"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("related_note_names"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, relatedNoteNamesProp)) { + obj["relatedNoteNames"] = relatedNoteNamesProp + } attestationAuthorityProp, err := expandContainerAnalysisNoteAttestationAuthority(d.Get("attestation_authority"), d, config) if err != nil { return err @@ -212,6 +366,13 @@ func resourceContainerAnalysisNoteUpdate(d *schema.ResourceData, meta interface{ return err } + lockName, err := replaceVars(d, config, "projects/{{project}}/notes/{{name}}") + if err != nil { + return err + } + mutexKV.Lock(lockName) + defer mutexKV.Unlock(lockName) + url, err := replaceVars(d, config, "{{ContainerAnalysisBasePath}}projects/{{project}}/notes/{{name}}") if err != nil { return err @@ -219,8 +380,29 @@ func resourceContainerAnalysisNoteUpdate(d *schema.ResourceData, meta interface{ log.Printf("[DEBUG] Updating Note %q: %#v", d.Id(), obj) updateMask := []string{} - if d.HasChange("attestation_authority.0.hint.0.human_readable_name") { - updateMask = append(updateMask, "attestationAuthority.hint.humanReadableName") + + if d.HasChange("short_description") { + updateMask = append(updateMask, "shortDescription") + } + + if d.HasChange("long_description") { + updateMask = append(updateMask, "longDescription") + } + + if d.HasChange("related_url") { + updateMask = append(updateMask, "relatedUrl") + } + + if d.HasChange("expiration_time") { + updateMask = append(updateMask, "expirationTime") + } + + if d.HasChange("related_note_names") { + updateMask = append(updateMask, "relatedNoteNames") + } + + if d.HasChange("attestation_authority") { + updateMask = append(updateMask, "attestationAuthority") } // updateMask is a URL parameter but not present in the schema, so replaceVars // won't set it @@ -245,6 +427,13 @@ func resourceContainerAnalysisNoteDelete(d *schema.ResourceData, meta interface{ return err } + lockName, err := replaceVars(d, config, "projects/{{project}}/notes/{{name}}") + if err != nil { + return err + } + mutexKV.Lock(lockName) + defer mutexKV.Unlock(lockName) + url, err := replaceVars(d, config, "{{ContainerAnalysisBasePath}}projects/{{project}}/notes/{{name}}") if err != nil { return err @@ -289,6 +478,64 @@ func flattenContainerAnalysisNoteName(v interface{}, d *schema.ResourceData, con return NameFromSelfLinkStateFunc(v) } +func flattenContainerAnalysisNoteShortDescription(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenContainerAnalysisNoteLongDescription(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenContainerAnalysisNoteKind(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenContainerAnalysisNoteRelatedUrl(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return v + } + l := v.([]interface{}) + transformed := schema.NewSet(schema.HashResource(containeranalysisNoteRelatedUrlSchema()), []interface{}{}) + for _, raw := range l { + original := raw.(map[string]interface{}) + if len(original) < 1 { + // Do not include empty json objects coming back from the api + continue + } + transformed.Add(map[string]interface{}{ + "url": flattenContainerAnalysisNoteRelatedUrlUrl(original["url"], d, config), + "label": flattenContainerAnalysisNoteRelatedUrlLabel(original["label"], d, config), + }) + } + return transformed +} +func flattenContainerAnalysisNoteRelatedUrlUrl(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenContainerAnalysisNoteRelatedUrlLabel(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenContainerAnalysisNoteExpirationTime(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenContainerAnalysisNoteCreateTime(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenContainerAnalysisNoteUpdateTime(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenContainerAnalysisNoteRelatedNoteNames(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return v + } + return schema.NewSet(schema.HashString, v.([]interface{})) +} + func flattenContainerAnalysisNoteAttestationAuthority(v interface{}, d *schema.ResourceData, config *Config) interface{} { if v == nil { return nil @@ -323,6 +570,61 @@ func expandContainerAnalysisNoteName(v interface{}, d TerraformResourceData, con return v, nil } +func expandContainerAnalysisNoteShortDescription(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandContainerAnalysisNoteLongDescription(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandContainerAnalysisNoteRelatedUrl(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + v = v.(*schema.Set).List() + l := v.([]interface{}) + req := make([]interface{}, 0, len(l)) + for _, raw := range l { + if raw == nil { + continue + } + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedUrl, err := expandContainerAnalysisNoteRelatedUrlUrl(original["url"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedUrl); val.IsValid() && !isEmptyValue(val) { + transformed["url"] = transformedUrl + } + + transformedLabel, err := expandContainerAnalysisNoteRelatedUrlLabel(original["label"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedLabel); val.IsValid() && !isEmptyValue(val) { + transformed["label"] = transformedLabel + } + + req = append(req, transformed) + } + return req, nil +} + +func expandContainerAnalysisNoteRelatedUrlUrl(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandContainerAnalysisNoteRelatedUrlLabel(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandContainerAnalysisNoteExpirationTime(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandContainerAnalysisNoteRelatedNoteNames(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + v = v.(*schema.Set).List() + return v, nil +} + func expandContainerAnalysisNoteAttestationAuthority(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { l := v.([]interface{}) if len(l) == 0 || l[0] == nil { diff --git a/google/resource_container_analysis_note_generated_test.go b/google/resource_container_analysis_note_generated_test.go index 25318efb445..da153f648d6 100644 --- a/google/resource_container_analysis_note_generated_test.go +++ b/google/resource_container_analysis_note_generated_test.go @@ -50,7 +50,58 @@ func TestAccContainerAnalysisNote_containerAnalysisNoteBasicExample(t *testing.T func testAccContainerAnalysisNote_containerAnalysisNoteBasicExample(context map[string]interface{}) string { return Nprintf(` resource "google_container_analysis_note" "note" { - name = "tf-test-test-attestor-note%{random_suffix}" + name = "tf-test-attestor-note%{random_suffix}" + attestation_authority { + hint { + human_readable_name = "Attestor Note" + } + } +} +`, context) +} + +func TestAccContainerAnalysisNote_containerAnalysisNoteAttestationFullExample(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": randString(t, 10), + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckContainerAnalysisNoteDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerAnalysisNote_containerAnalysisNoteAttestationFullExample(context), + }, + { + ResourceName: "google_container_analysis_note.note", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccContainerAnalysisNote_containerAnalysisNoteAttestationFullExample(context map[string]interface{}) string { + return Nprintf(` +resource "google_container_analysis_note" "note" { + name = "tf-test-attestor-note%{random_suffix}" + + short_description = "test note" + long_description = "a longer description of test note" + expiration_time = "2120-10-02T15:01:23.045123456Z" + + related_url { + url = "some.url" + label = "foo" + } + + related_url { + url = "google.com" + } + attestation_authority { hint { human_readable_name = "Attestor Note" diff --git a/google/resource_container_analysis_occurrence.go b/google/resource_container_analysis_occurrence.go new file mode 100644 index 00000000000..666b7539b2e --- /dev/null +++ b/google/resource_container_analysis_occurrence.go @@ -0,0 +1,591 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package google + +import ( + "fmt" + "log" + "reflect" + "strings" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func resourceContainerAnalysisOccurrence() *schema.Resource { + return &schema.Resource{ + Create: resourceContainerAnalysisOccurrenceCreate, + Read: resourceContainerAnalysisOccurrenceRead, + Update: resourceContainerAnalysisOccurrenceUpdate, + Delete: resourceContainerAnalysisOccurrenceDelete, + + Importer: &schema.ResourceImporter{ + State: resourceContainerAnalysisOccurrenceImport, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(4 * time.Minute), + Update: schema.DefaultTimeout(4 * time.Minute), + Delete: schema.DefaultTimeout(4 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "attestation": { + Type: schema.TypeList, + Required: true, + Description: `Occurrence that represents a single "attestation". The authenticity +of an attestation can be verified using the attached signature. +If the verifier trusts the public key of the signer, then verifying +the signature is sufficient to establish trust. In this circumstance, +the authority to which this attestation is attached is primarily +useful for lookup (how to find this attestation if you already +know the authority and artifact to be verified) and intent (for +which authority this attestation was intended to sign.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "serialized_payload": { + Type: schema.TypeString, + Required: true, + Description: `The serialized payload that is verified by one or +more signatures. A base64-encoded string.`, + }, + "signatures": { + Type: schema.TypeSet, + Required: true, + Description: `One or more signatures over serializedPayload. +Verifier implementations should consider this attestation +message verified if at least one signature verifies +serializedPayload. See Signature in common.proto for more +details on signature structure and verification.`, + Elem: containeranalysisOccurrenceAttestationSignaturesSchema(), + // Default schema.HashSchema is used. + }, + }, + }, + }, + "note_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The analysis note associated with this occurrence, in the form of +projects/[PROJECT]/notes/[NOTE_ID]. This field can be used as a +filter in list requests.`, + }, + "resource_uri": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `Required. Immutable. A URI that represents the resource for which +the occurrence applies. For example, +https://gcr.io/project/image@sha256:123abc for a Docker image.`, + }, + "remediation": { + Type: schema.TypeString, + Optional: true, + Description: `A description of actions that can be taken to remedy the note.`, + }, + "create_time": { + Type: schema.TypeString, + Computed: true, + Description: `The time when the repository was created.`, + }, + "kind": { + Type: schema.TypeString, + Computed: true, + Description: `The note kind which explicitly denotes which of the occurrence +details are specified. This field can be used as a filter in list +requests.`, + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: `The name of the occurrence.`, + }, + "update_time": { + Type: schema.TypeString, + Computed: true, + Description: `The time when the repository was last updated.`, + }, + "project": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + }, + } +} + +func containeranalysisOccurrenceAttestationSignaturesSchema() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "public_key_id": { + Type: schema.TypeString, + Required: true, + Description: `The identifier for the public key that verifies this +signature. MUST be an RFC3986 conformant +URI. * When possible, the key id should be an +immutable reference, such as a cryptographic digest. +Examples of valid values: + +* OpenPGP V4 public key fingerprint. See https://www.iana.org/assignments/uri-schemes/prov/openpgp4fpr + for more details on this scheme. + * 'openpgp4fpr:74FAF3B861BDA0870C7B6DEF607E48D2A663AEEA' +* RFC6920 digest-named SubjectPublicKeyInfo (digest of the DER serialization): + * "ni:///sha-256;cD9o9Cq6LG3jD0iKXqEi_vdjJGecm_iXkbqVoScViaU"`, + }, + "signature": { + Type: schema.TypeString, + Optional: true, + Description: `The content of the signature, an opaque bytestring. +The payload that this signature verifies MUST be +unambiguously provided with the Signature during +verification. A wrapper message might provide the +payload explicitly. Alternatively, a message might +have a canonical serialization that can always be +unambiguously computed to derive the payload.`, + }, + }, + } +} + +func resourceContainerAnalysisOccurrenceCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + obj := make(map[string]interface{}) + resourceUriProp, err := expandContainerAnalysisOccurrenceResourceUri(d.Get("resource_uri"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("resource_uri"); !isEmptyValue(reflect.ValueOf(resourceUriProp)) && (ok || !reflect.DeepEqual(v, resourceUriProp)) { + obj["resourceUri"] = resourceUriProp + } + noteNameProp, err := expandContainerAnalysisOccurrenceNoteName(d.Get("note_name"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("note_name"); !isEmptyValue(reflect.ValueOf(noteNameProp)) && (ok || !reflect.DeepEqual(v, noteNameProp)) { + obj["noteName"] = noteNameProp + } + remediationProp, err := expandContainerAnalysisOccurrenceRemediation(d.Get("remediation"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("remediation"); !isEmptyValue(reflect.ValueOf(remediationProp)) && (ok || !reflect.DeepEqual(v, remediationProp)) { + obj["remediation"] = remediationProp + } + attestationProp, err := expandContainerAnalysisOccurrenceAttestation(d.Get("attestation"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("attestation"); !isEmptyValue(reflect.ValueOf(attestationProp)) && (ok || !reflect.DeepEqual(v, attestationProp)) { + obj["attestation"] = attestationProp + } + + obj, err = resourceContainerAnalysisOccurrenceEncoder(d, meta, obj) + if err != nil { + return err + } + + lockName, err := replaceVars(d, config, "{{note_name}}") + if err != nil { + return err + } + mutexKV.Lock(lockName) + defer mutexKV.Unlock(lockName) + + url, err := replaceVars(d, config, "{{ContainerAnalysisBasePath}}projects/{{project}}/occurrences") + if err != nil { + return err + } + + log.Printf("[DEBUG] Creating new Occurrence: %#v", obj) + project, err := getProject(d, config) + if err != nil { + return err + } + res, err := sendRequestWithTimeout(config, "POST", project, url, obj, d.Timeout(schema.TimeoutCreate)) + if err != nil { + return fmt.Errorf("Error creating Occurrence: %s", err) + } + if err := d.Set("name", flattenContainerAnalysisOccurrenceName(res["name"], d, config)); err != nil { + return fmt.Errorf(`Error setting computed identity field "name": %s`, err) + } + + // Store the ID now + id, err := replaceVars(d, config, "projects/{{project}}/occurrences/{{name}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + log.Printf("[DEBUG] Finished creating Occurrence %q: %#v", d.Id(), res) + + return resourceContainerAnalysisOccurrenceRead(d, meta) +} + +func resourceContainerAnalysisOccurrenceRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + url, err := replaceVars(d, config, "{{ContainerAnalysisBasePath}}projects/{{project}}/occurrences/{{name}}") + if err != nil { + return err + } + + project, err := getProject(d, config) + if err != nil { + return err + } + res, err := sendRequest(config, "GET", project, url, nil) + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("ContainerAnalysisOccurrence %q", d.Id())) + } + + res, err = resourceContainerAnalysisOccurrenceDecoder(d, meta, res) + if err != nil { + return err + } + + if res == nil { + // Decoding the object has resulted in it being gone. It may be marked deleted + log.Printf("[DEBUG] Removing ContainerAnalysisOccurrence because it no longer exists.") + d.SetId("") + return nil + } + + if err := d.Set("project", project); err != nil { + return fmt.Errorf("Error reading Occurrence: %s", err) + } + + if err := d.Set("name", flattenContainerAnalysisOccurrenceName(res["name"], d, config)); err != nil { + return fmt.Errorf("Error reading Occurrence: %s", err) + } + if err := d.Set("resource_uri", flattenContainerAnalysisOccurrenceResourceUri(res["resourceUri"], d, config)); err != nil { + return fmt.Errorf("Error reading Occurrence: %s", err) + } + if err := d.Set("note_name", flattenContainerAnalysisOccurrenceNoteName(res["noteName"], d, config)); err != nil { + return fmt.Errorf("Error reading Occurrence: %s", err) + } + if err := d.Set("kind", flattenContainerAnalysisOccurrenceKind(res["kind"], d, config)); err != nil { + return fmt.Errorf("Error reading Occurrence: %s", err) + } + if err := d.Set("remediation", flattenContainerAnalysisOccurrenceRemediation(res["remediation"], d, config)); err != nil { + return fmt.Errorf("Error reading Occurrence: %s", err) + } + if err := d.Set("create_time", flattenContainerAnalysisOccurrenceCreateTime(res["createTime"], d, config)); err != nil { + return fmt.Errorf("Error reading Occurrence: %s", err) + } + if err := d.Set("update_time", flattenContainerAnalysisOccurrenceUpdateTime(res["updateTime"], d, config)); err != nil { + return fmt.Errorf("Error reading Occurrence: %s", err) + } + if err := d.Set("attestation", flattenContainerAnalysisOccurrenceAttestation(res["attestation"], d, config)); err != nil { + return fmt.Errorf("Error reading Occurrence: %s", err) + } + + return nil +} + +func resourceContainerAnalysisOccurrenceUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + project, err := getProject(d, config) + if err != nil { + return err + } + + obj := make(map[string]interface{}) + remediationProp, err := expandContainerAnalysisOccurrenceRemediation(d.Get("remediation"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("remediation"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, remediationProp)) { + obj["remediation"] = remediationProp + } + attestationProp, err := expandContainerAnalysisOccurrenceAttestation(d.Get("attestation"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("attestation"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, attestationProp)) { + obj["attestation"] = attestationProp + } + + obj, err = resourceContainerAnalysisOccurrenceUpdateEncoder(d, meta, obj) + if err != nil { + return err + } + + lockName, err := replaceVars(d, config, "{{note_name}}") + if err != nil { + return err + } + mutexKV.Lock(lockName) + defer mutexKV.Unlock(lockName) + + url, err := replaceVars(d, config, "{{ContainerAnalysisBasePath}}projects/{{project}}/occurrences/{{name}}") + if err != nil { + return err + } + + log.Printf("[DEBUG] Updating Occurrence %q: %#v", d.Id(), obj) + updateMask := []string{} + + if d.HasChange("remediation") { + updateMask = append(updateMask, "remediation") + } + + if d.HasChange("attestation") { + updateMask = append(updateMask, "attestation") + } + // updateMask is a URL parameter but not present in the schema, so replaceVars + // won't set it + url, err = addQueryParams(url, map[string]string{"updateMask": strings.Join(updateMask, ",")}) + if err != nil { + return err + } + _, err = sendRequestWithTimeout(config, "PATCH", project, url, obj, d.Timeout(schema.TimeoutUpdate)) + + if err != nil { + return fmt.Errorf("Error updating Occurrence %q: %s", d.Id(), err) + } + + return resourceContainerAnalysisOccurrenceRead(d, meta) +} + +func resourceContainerAnalysisOccurrenceDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + project, err := getProject(d, config) + if err != nil { + return err + } + + lockName, err := replaceVars(d, config, "{{note_name}}") + if err != nil { + return err + } + mutexKV.Lock(lockName) + defer mutexKV.Unlock(lockName) + + url, err := replaceVars(d, config, "{{ContainerAnalysisBasePath}}projects/{{project}}/occurrences/{{name}}") + if err != nil { + return err + } + + var obj map[string]interface{} + log.Printf("[DEBUG] Deleting Occurrence %q", d.Id()) + + res, err := sendRequestWithTimeout(config, "DELETE", project, url, obj, d.Timeout(schema.TimeoutDelete)) + if err != nil { + return handleNotFoundError(err, d, "Occurrence") + } + + log.Printf("[DEBUG] Finished deleting Occurrence %q: %#v", d.Id(), res) + return nil +} + +func resourceContainerAnalysisOccurrenceImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + config := meta.(*Config) + if err := parseImportId([]string{ + "projects/(?P[^/]+)/occurrences/(?P[^/]+)", + "(?P[^/]+)/(?P[^/]+)", + "(?P[^/]+)", + }, d, config); err != nil { + return nil, err + } + + // Replace import id for the resource id + id, err := replaceVars(d, config, "projects/{{project}}/occurrences/{{name}}") + if err != nil { + return nil, fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + return []*schema.ResourceData{d}, nil +} + +func flattenContainerAnalysisOccurrenceName(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return v + } + return NameFromSelfLinkStateFunc(v) +} + +func flattenContainerAnalysisOccurrenceResourceUri(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenContainerAnalysisOccurrenceNoteName(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenContainerAnalysisOccurrenceKind(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenContainerAnalysisOccurrenceRemediation(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenContainerAnalysisOccurrenceCreateTime(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenContainerAnalysisOccurrenceUpdateTime(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenContainerAnalysisOccurrenceAttestation(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["serialized_payload"] = + flattenContainerAnalysisOccurrenceAttestationSerializedPayload(original["serializedPayload"], d, config) + transformed["signatures"] = + flattenContainerAnalysisOccurrenceAttestationSignatures(original["signatures"], d, config) + return []interface{}{transformed} +} +func flattenContainerAnalysisOccurrenceAttestationSerializedPayload(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenContainerAnalysisOccurrenceAttestationSignatures(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return v + } + l := v.([]interface{}) + transformed := schema.NewSet(schema.HashResource(containeranalysisOccurrenceAttestationSignaturesSchema()), []interface{}{}) + for _, raw := range l { + original := raw.(map[string]interface{}) + if len(original) < 1 { + // Do not include empty json objects coming back from the api + continue + } + transformed.Add(map[string]interface{}{ + "signature": flattenContainerAnalysisOccurrenceAttestationSignaturesSignature(original["signature"], d, config), + "public_key_id": flattenContainerAnalysisOccurrenceAttestationSignaturesPublicKeyId(original["publicKeyId"], d, config), + }) + } + return transformed +} +func flattenContainerAnalysisOccurrenceAttestationSignaturesSignature(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenContainerAnalysisOccurrenceAttestationSignaturesPublicKeyId(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func expandContainerAnalysisOccurrenceResourceUri(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandContainerAnalysisOccurrenceNoteName(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandContainerAnalysisOccurrenceRemediation(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandContainerAnalysisOccurrenceAttestation(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedSerializedPayload, err := expandContainerAnalysisOccurrenceAttestationSerializedPayload(original["serialized_payload"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedSerializedPayload); val.IsValid() && !isEmptyValue(val) { + transformed["serializedPayload"] = transformedSerializedPayload + } + + transformedSignatures, err := expandContainerAnalysisOccurrenceAttestationSignatures(original["signatures"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedSignatures); val.IsValid() && !isEmptyValue(val) { + transformed["signatures"] = transformedSignatures + } + + return transformed, nil +} + +func expandContainerAnalysisOccurrenceAttestationSerializedPayload(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandContainerAnalysisOccurrenceAttestationSignatures(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + v = v.(*schema.Set).List() + l := v.([]interface{}) + req := make([]interface{}, 0, len(l)) + for _, raw := range l { + if raw == nil { + continue + } + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedSignature, err := expandContainerAnalysisOccurrenceAttestationSignaturesSignature(original["signature"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedSignature); val.IsValid() && !isEmptyValue(val) { + transformed["signature"] = transformedSignature + } + + transformedPublicKeyId, err := expandContainerAnalysisOccurrenceAttestationSignaturesPublicKeyId(original["public_key_id"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedPublicKeyId); val.IsValid() && !isEmptyValue(val) { + transformed["publicKeyId"] = transformedPublicKeyId + } + + req = append(req, transformed) + } + return req, nil +} + +func expandContainerAnalysisOccurrenceAttestationSignaturesSignature(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandContainerAnalysisOccurrenceAttestationSignaturesPublicKeyId(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func resourceContainerAnalysisOccurrenceEncoder(d *schema.ResourceData, meta interface{}, obj map[string]interface{}) (map[string]interface{}, error) { + // encoder logic only in non-GA versions + + return obj, nil +} + +func resourceContainerAnalysisOccurrenceUpdateEncoder(d *schema.ResourceData, meta interface{}, obj map[string]interface{}) (map[string]interface{}, error) { + // Note is required, even for PATCH + noteNameProp, err := expandContainerAnalysisOccurrenceNoteName(d.Get("note_name"), d, meta.(*Config)) + if err != nil { + return nil, err + } else if v, ok := d.GetOkExists("note_name"); !isEmptyValue(reflect.ValueOf(noteNameProp)) && (ok || !reflect.DeepEqual(v, noteNameProp)) { + obj["noteName"] = noteNameProp + } + + return resourceContainerAnalysisOccurrenceEncoder(d, meta, obj) +} + +func resourceContainerAnalysisOccurrenceDecoder(d *schema.ResourceData, meta interface{}, res map[string]interface{}) (map[string]interface{}, error) { + // encoder logic only in non-GA version + return res, nil +} diff --git a/google/resource_container_analysis_occurrence_sweeper_test.go b/google/resource_container_analysis_occurrence_sweeper_test.go new file mode 100644 index 00000000000..e5dea1273ef --- /dev/null +++ b/google/resource_container_analysis_occurrence_sweeper_test.go @@ -0,0 +1,124 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package google + +import ( + "context" + "log" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" +) + +func init() { + resource.AddTestSweepers("ContainerAnalysisOccurrence", &resource.Sweeper{ + Name: "ContainerAnalysisOccurrence", + F: testSweepContainerAnalysisOccurrence, + }) +} + +// At the time of writing, the CI only passes us-central1 as the region +func testSweepContainerAnalysisOccurrence(region string) error { + resourceName := "ContainerAnalysisOccurrence" + log.Printf("[INFO][SWEEPER_LOG] Starting sweeper for %s", resourceName) + + config, err := sharedConfigForRegion(region) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error getting shared config for region: %s", err) + return err + } + + err = config.LoadAndValidate(context.Background()) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error loading: %s", err) + return err + } + + t := &testing.T{} + billingId := getTestBillingAccountFromEnv(t) + + // Setup variables to replace in list template + d := &ResourceDataMock{ + FieldsInSchema: map[string]interface{}{ + "project": config.Project, + "region": region, + "location": region, + "zone": "-", + "billing_account": billingId, + }, + } + + listTemplate := strings.Split("https://containeranalysis.googleapis.com/v1/projects/{{project}}/occurrences", "?")[0] + listUrl, err := replaceVars(d, config, listTemplate) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error preparing sweeper list url: %s", err) + return nil + } + + res, err := sendRequest(config, "GET", config.Project, listUrl, nil) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] Error in response from request %s: %s", listUrl, err) + return nil + } + + resourceList, ok := res["occurrences"] + if !ok { + log.Printf("[INFO][SWEEPER_LOG] Nothing found in response.") + return nil + } + + rl := resourceList.([]interface{}) + + log.Printf("[INFO][SWEEPER_LOG] Found %d items in %s list response.", len(rl), resourceName) + // Keep count of items that aren't sweepable for logging. + nonPrefixCount := 0 + for _, ri := range rl { + obj := ri.(map[string]interface{}) + if obj["name"] == nil { + log.Printf("[INFO][SWEEPER_LOG] %s resource name was nil", resourceName) + return nil + } + + name := GetResourceNameFromSelfLink(obj["name"].(string)) + // Skip resources that shouldn't be sweeped + if !isSweepableTestResource(name) { + nonPrefixCount++ + continue + } + + deleteTemplate := "https://containeranalysis.googleapis.com/v1/projects/{{project}}/occurrences/{{name}}" + deleteUrl, err := replaceVars(d, config, deleteTemplate) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error preparing delete url: %s", err) + return nil + } + deleteUrl = deleteUrl + name + + // Don't wait on operations as we may have a lot to delete + _, err = sendRequest(config, "DELETE", config.Project, deleteUrl, nil) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] Error deleting for url %s : %s", deleteUrl, err) + } else { + log.Printf("[INFO][SWEEPER_LOG] Sent delete request for %s resource: %s", resourceName, name) + } + } + + if nonPrefixCount > 0 { + log.Printf("[INFO][SWEEPER_LOG] %d items were non-sweepable and skipped.", nonPrefixCount) + } + + return nil +} diff --git a/google/resource_container_analysis_occurrence_test.go b/google/resource_container_analysis_occurrence_test.go new file mode 100644 index 00000000000..f095d0c2428 --- /dev/null +++ b/google/resource_container_analysis_occurrence_test.go @@ -0,0 +1,277 @@ +package google + +import ( + "encoding/base64" + "fmt" + "io/ioutil" + "testing" + + "crypto/sha512" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "google.golang.org/api/cloudkms/v1" +) + +const testAttestationOccurrenceImageUrl = "gcr.io/cloud-marketplace/google/ubuntu1804" +const testAttestationOccurrenceImageDigest = "sha256:3593cd4ac7d782d460dc86ba9870a3beaf81c8f5cdbcc8880bf9a5ef6af10c5a" +const testAttestationOccurrencePayloadTemplate = "test-fixtures/binauthz/generated_payload.json.tmpl" + +var testAttestationOccurrenceFullImagePath = fmt.Sprintf("%s@%s", testAttestationOccurrenceImageUrl, testAttestationOccurrenceImageDigest) + +func getTestOccurrenceAttestationPayload(t *testing.T) string { + payloadTmpl, err := ioutil.ReadFile(testAttestationOccurrencePayloadTemplate) + if err != nil { + t.Fatal(err.Error()) + } + return fmt.Sprintf(string(payloadTmpl), + testAttestationOccurrenceImageUrl, + testAttestationOccurrenceImageDigest) +} + +func getSignedTestOccurrenceAttestationPayload( + t *testing.T, config *Config, + signingKey bootstrappedKMS, rawPayload string) string { + pbytes := []byte(rawPayload) + ssum := sha512.Sum512(pbytes) + hashed := base64.StdEncoding.EncodeToString(ssum[:]) + signed, err := config.clientKms.Projects.Locations.KeyRings.CryptoKeys. + CryptoKeyVersions.AsymmetricSign( + fmt.Sprintf("%s/cryptoKeyVersions/1", signingKey.CryptoKey.Name), + &cloudkms.AsymmetricSignRequest{ + Digest: &cloudkms.Digest{ + Sha512: hashed, + }, + }).Do() + if err != nil { + t.Fatalf("Unable to sign attestation payload with KMS key: %s", err) + } + + return signed.Signature +} + +func TestAccContainerAnalysisOccurrence_basic(t *testing.T) { + t.Parallel() + randSuffix := randString(t, 10) + + config := BootstrapConfig(t) + if config == nil { + return + } + + signKey := BootstrapKMSKeyWithPurpose(t, "ASYMMETRIC_SIGN") + payload := getTestOccurrenceAttestationPayload(t) + signed := getSignedTestOccurrenceAttestationPayload(t, config, signKey, payload) + params := map[string]interface{}{ + "random_suffix": randSuffix, + "image_url": testAttestationOccurrenceFullImagePath, + "key_ring": GetResourceNameFromSelfLink(signKey.KeyRing.Name), + "crypto_key": GetResourceNameFromSelfLink(signKey.CryptoKey.Name), + "payload": base64.StdEncoding.EncodeToString([]byte(payload)), + "signature": base64.StdEncoding.EncodeToString([]byte(signed)), + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckContainerAnalysisNoteDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerAnalysisOccurence_basic(params), + }, + { + ResourceName: "google_container_analysis_occurrence.occurrence", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccContainerAnalysisOccurrence_multipleSignatures(t *testing.T) { + t.Parallel() + randSuffix := randString(t, 10) + + config := BootstrapConfig(t) + if config == nil { + return + } + + payload := getTestOccurrenceAttestationPayload(t) + key1 := BootstrapKMSKeyWithPurposeInLocationAndName(t, "ASYMMETRIC_SIGN", "global", "tf-bootstrap-binauthz-key1") + signature1 := getSignedTestOccurrenceAttestationPayload(t, config, key1, payload) + + key2 := BootstrapKMSKeyWithPurposeInLocationAndName(t, "ASYMMETRIC_SIGN", "global", "tf-bootstrap-binauthz-key2") + signature2 := getSignedTestOccurrenceAttestationPayload(t, config, key2, payload) + + paramsMultipleSignatures := map[string]interface{}{ + "random_suffix": randSuffix, + "image_url": testAttestationOccurrenceFullImagePath, + "key_ring": GetResourceNameFromSelfLink(key1.KeyRing.Name), + "payload": base64.StdEncoding.EncodeToString([]byte(payload)), + "key1": GetResourceNameFromSelfLink(key1.CryptoKey.Name), + "signature1": base64.StdEncoding.EncodeToString([]byte(signature1)), + "key2": GetResourceNameFromSelfLink(key2.CryptoKey.Name), + "signature2": base64.StdEncoding.EncodeToString([]byte(signature2)), + } + paramsSingle := map[string]interface{}{ + "random_suffix": randSuffix, + "image_url": testAttestationOccurrenceFullImagePath, + "key_ring": GetResourceNameFromSelfLink(key1.KeyRing.Name), + "crypto_key": GetResourceNameFromSelfLink(key1.CryptoKey.Name), + "payload": base64.StdEncoding.EncodeToString([]byte(payload)), + "signature": base64.StdEncoding.EncodeToString([]byte(signature1)), + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckContainerAnalysisNoteDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerAnalysisOccurence_multipleSignatures(paramsMultipleSignatures), + }, + { + ResourceName: "google_container_analysis_occurrence.occurrence", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccContainerAnalysisOccurence_basic(paramsSingle), + }, + { + ResourceName: "google_container_analysis_occurrence.occurrence", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccContainerAnalysisOccurence_basic(params map[string]interface{}) string { + return Nprintf(` +resource "google_binary_authorization_attestor" "attestor" { + name = "test-attestor%{random_suffix}" + attestation_authority_note { + note_reference = google_container_analysis_note.note.name + public_keys { + id = data.google_kms_crypto_key_version.version.id + pkix_public_key { + public_key_pem = data.google_kms_crypto_key_version.version.public_key[0].pem + signature_algorithm = data.google_kms_crypto_key_version.version.public_key[0].algorithm + } + } + } +} + +resource "google_container_analysis_note" "note" { + name = "test-attestor-note%{random_suffix}" + attestation_authority { + hint { + human_readable_name = "Attestor Note" + } + } +} + +data "google_kms_key_ring" "keyring" { + name = "%{key_ring}" + location = "global" +} + +data "google_kms_crypto_key" "crypto-key" { + name = "%{crypto_key}" + key_ring = data.google_kms_key_ring.keyring.self_link +} + +data "google_kms_crypto_key_version" "version" { + crypto_key = data.google_kms_crypto_key.crypto-key.self_link +} + +resource "google_container_analysis_occurrence" "occurrence" { + resource_uri = "%{image_url}" + note_name = google_container_analysis_note.note.id + + attestation { + serialized_payload = "%{payload}" + signatures { + public_key_id = data.google_kms_crypto_key_version.version.id + signature = "%{signature}" + } + } +} +`, params) +} + +func testAccContainerAnalysisOccurence_multipleSignatures(params map[string]interface{}) string { + return Nprintf(` +resource "google_binary_authorization_attestor" "attestor" { + name = "test-attestor%{random_suffix}" + attestation_authority_note { + note_reference = google_container_analysis_note.note.name + public_keys { + id = data.google_kms_crypto_key_version.version-key1.id + pkix_public_key { + public_key_pem = data.google_kms_crypto_key_version.version-key1.public_key[0].pem + signature_algorithm = data.google_kms_crypto_key_version.version-key1.public_key[0].algorithm + } + } + + public_keys { + id = data.google_kms_crypto_key_version.version-key2.id + pkix_public_key { + public_key_pem = data.google_kms_crypto_key_version.version-key2.public_key[0].pem + signature_algorithm = data.google_kms_crypto_key_version.version-key2.public_key[0].algorithm + } + } + } +} + +resource "google_container_analysis_note" "note" { + name = "test-attestor-note%{random_suffix}" + attestation_authority { + hint { + human_readable_name = "Attestor Note" + } + } +} + +data "google_kms_key_ring" "keyring" { + name = "%{key_ring}" + location = "global" +} + +data "google_kms_crypto_key" "crypto-key1" { + name = "%{key1}" + key_ring = data.google_kms_key_ring.keyring.self_link +} + +data "google_kms_crypto_key" "crypto-key2" { + name = "%{key2}" + key_ring = data.google_kms_key_ring.keyring.self_link +} + +data "google_kms_crypto_key_version" "version-key1" { + crypto_key = data.google_kms_crypto_key.crypto-key1.self_link +} + +data "google_kms_crypto_key_version" "version-key2" { + crypto_key = data.google_kms_crypto_key.crypto-key2.self_link +} + +resource "google_container_analysis_occurrence" "occurrence" { + resource_uri = "%{image_url}" + note_name = google_container_analysis_note.note.id + + attestation { + serialized_payload = "%{payload}" + signatures { + public_key_id = data.google_kms_crypto_key_version.version-key1.id + signature = "%{signature1}" + } + + signatures { + public_key_id = data.google_kms_crypto_key_version.version-key2.id + signature = "%{signature2}" + } + } +} +`, params) +} diff --git a/google/test-fixtures/binauthz/generated_payload.json.tmpl b/google/test-fixtures/binauthz/generated_payload.json.tmpl new file mode 100644 index 00000000000..3db3c90fe98 --- /dev/null +++ b/google/test-fixtures/binauthz/generated_payload.json.tmpl @@ -0,0 +1,12 @@ +{ + "critical": { + "identity": { + "docker-reference": "%s" + }, + "image": { + "%s" + }, + "type": "Google cloud binauthz container signature" + } +} + diff --git a/website/docs/r/container_analysis_note.html.markdown b/website/docs/r/container_analysis_note.html.markdown index cab0ba45fc4..8ce09113085 100644 --- a/website/docs/r/container_analysis_note.html.markdown +++ b/website/docs/r/container_analysis_note.html.markdown @@ -17,12 +17,14 @@ layout: "google" page_title: "Google: google_container_analysis_note" sidebar_current: "docs-google-container-analysis-note" description: |- - Provides a detailed description of a Note. + A Container Analysis note is a high-level piece of metadata that + describes a type of analysis that can be done for a resource. --- # google\_container\_analysis\_note -Provides a detailed description of a Note. +A Container Analysis note is a high-level piece of metadata that +describes a type of analysis that can be done for a resource. To get more information about Note, see: @@ -30,6 +32,7 @@ To get more information about Note, see: * [API documentation](https://cloud.google.com/container-analysis/api/reference/rest/) * How-to Guides * [Official Documentation](https://cloud.google.com/container-analysis/) + * [Creating Attestations (Occurrences)](https://cloud.google.com/binary-authorization/docs/making-attestations)
@@ -41,7 +44,39 @@ To get more information about Note, see: ```hcl resource "google_container_analysis_note" "note" { - name = "test-attestor-note" + name = "attestor-note" + attestation_authority { + hint { + human_readable_name = "Attestor Note" + } + } +} +``` + +## Example Usage - Container Analysis Note Attestation Full + + +```hcl +resource "google_container_analysis_note" "note" { + name = "attestor-note" + + short_description = "test note" + long_description = "a longer description of test note" + expiration_time = "2120-10-02T15:01:23.045123456Z" + + related_url { + url = "some.url" + label = "foo" + } + + related_url { + url = "google.com" + } + attestation_authority { hint { human_readable_name = "Attestor Note" @@ -96,16 +131,55 @@ The `hint` block supports: - - - +* `short_description` - + (Optional) + A one sentence description of the note. + +* `long_description` - + (Optional) + A detailed description of the note + +* `related_url` - + (Optional) + URLs associated with this note and related metadata. Structure is documented below. + +* `expiration_time` - + (Optional) + Time of expiration for this note. Leave empty if note does not expire. + +* `related_note_names` - + (Optional) + Names of other notes related to this note. + * `project` - (Optional) The ID of the project in which the resource belongs. If it is not provided, the provider project is used. +The `related_url` block supports: + +* `url` - + (Required) + Specific URL associated with the resource. + +* `label` - + (Optional) + Label to describe usage of the URL + ## Attributes Reference In addition to the arguments listed above, the following computed attributes are exported: * `id` - an identifier for the resource with format `projects/{{project}}/notes/{{name}}` +* `kind` - + The type of analysis this note describes + +* `create_time` - + The time this note was created. + +* `update_time` - + The time this note was last updated. + ## Timeouts diff --git a/website/docs/r/container_analysis_occurrence.html.markdown b/website/docs/r/container_analysis_occurrence.html.markdown new file mode 100644 index 00000000000..59f571e4f30 --- /dev/null +++ b/website/docs/r/container_analysis_occurrence.html.markdown @@ -0,0 +1,219 @@ +--- +# ---------------------------------------------------------------------------- +# +# *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +# +# ---------------------------------------------------------------------------- +# +# This file is automatically generated by Magic Modules and manual +# changes will be clobbered when the file is regenerated. +# +# Please read more about how to change this file in +# .github/CONTRIBUTING.md. +# +# ---------------------------------------------------------------------------- +subcategory: "Container Registry" +layout: "google" +page_title: "Google: google_container_analysis_occurrence" +sidebar_current: "docs-google-container-analysis-occurrence" +description: |- + An occurrence is an instance of a Note, or type of analysis that + can be done for a resource. +--- + +# google\_container\_analysis\_occurrence + +An occurrence is an instance of a Note, or type of analysis that +can be done for a resource. + + +To get more information about Occurrence, see: + +* [API documentation](https://cloud.google.com/container-analysis/api/reference/rest/) +* How-to Guides + * [Official Documentation](https://cloud.google.com/container-analysis/) + +## Example Usage - Container Analysis Occurrence Kms + + +```hcl +resource "google_binary_authorization_attestor" "attestor" { + name = "attestor" + attestation_authority_note { + note_reference = google_container_analysis_note.note.name + public_keys { + id = data.google_kms_crypto_key_version.version.id + pkix_public_key { + public_key_pem = data.google_kms_crypto_key_version.version.public_key[0].pem + signature_algorithm = data.google_kms_crypto_key_version.version.public_key[0].algorithm + } + } + } +} + +resource "google_container_analysis_note" "note" { + name = "attestation-note" + attestation_authority { + hint { + human_readable_name = "Attestor Note" + } + } +} + +data "google_kms_key_ring" "keyring" { + name = "my-key-ring" + location = "global" +} + +data "google_kms_crypto_key" "crypto-key" { + name = "my-key" + key_ring = data.google_kms_key_ring.keyring.self_link +} + +data "google_kms_crypto_key_version" "version" { + crypto_key = data.google_kms_crypto_key.crypto-key.self_link +} + +resource "google_container_analysis_occurrence" "occurrence" { + resource_uri = "gcr.io/my-project/my-image" + note_name = google_container_analysis_note.note.id + + // See "Creating Attestations" Guide for expected + // payload and signature formats. + attestation { + serialized_payload = filebase64("path/to/my/payload.json") + signatures { + public_key_id = data.google_kms_crypto_key_version.version.id + serialized_payload = filebase64("path/to/my/payload.json.sig") + } + } +} +``` + +## Argument Reference + +The following arguments are supported: + + +* `resource_uri` - + (Required) + Required. Immutable. A URI that represents the resource for which + the occurrence applies. For example, + https://gcr.io/project/image@sha256:123abc for a Docker image. + +* `note_name` - + (Required) + The analysis note associated with this occurrence, in the form of + projects/[PROJECT]/notes/[NOTE_ID]. This field can be used as a + filter in list requests. + +* `attestation` - + (Required) + Occurrence that represents a single "attestation". The authenticity + of an attestation can be verified using the attached signature. + If the verifier trusts the public key of the signer, then verifying + the signature is sufficient to establish trust. In this circumstance, + the authority to which this attestation is attached is primarily + useful for lookup (how to find this attestation if you already + know the authority and artifact to be verified) and intent (for + which authority this attestation was intended to sign. Structure is documented below. + + +The `attestation` block supports: + +* `serialized_payload` - + (Required) + The serialized payload that is verified by one or + more signatures. A base64-encoded string. + +* `signatures` - + (Required) + One or more signatures over serializedPayload. + Verifier implementations should consider this attestation + message verified if at least one signature verifies + serializedPayload. See Signature in common.proto for more + details on signature structure and verification. Structure is documented below. + + +The `signatures` block supports: + +* `signature` - + (Optional) + The content of the signature, an opaque bytestring. + The payload that this signature verifies MUST be + unambiguously provided with the Signature during + verification. A wrapper message might provide the + payload explicitly. Alternatively, a message might + have a canonical serialization that can always be + unambiguously computed to derive the payload. + +* `public_key_id` - + (Required) + The identifier for the public key that verifies this + signature. MUST be an RFC3986 conformant + URI. * When possible, the key id should be an + immutable reference, such as a cryptographic digest. + Examples of valid values: + * OpenPGP V4 public key fingerprint. See https://www.iana.org/assignments/uri-schemes/prov/openpgp4fpr + for more details on this scheme. + * `openpgp4fpr:74FAF3B861BDA0870C7B6DEF607E48D2A663AEEA` + * RFC6920 digest-named SubjectPublicKeyInfo (digest of the DER serialization): + * "ni:///sha-256;cD9o9Cq6LG3jD0iKXqEi_vdjJGecm_iXkbqVoScViaU" + +- - - + + +* `remediation` - + (Optional) + A description of actions that can be taken to remedy the note. + +* `project` - (Optional) The ID of the project in which the resource belongs. + If it is not provided, the provider project is used. + + +## Attributes Reference + +In addition to the arguments listed above, the following computed attributes are exported: + +* `id` - an identifier for the resource with format `projects/{{project}}/occurrences/{{name}}` + +* `name` - + The name of the occurrence. + +* `kind` - + The note kind which explicitly denotes which of the occurrence + details are specified. This field can be used as a filter in list + requests. + +* `create_time` - + The time when the repository was created. + +* `update_time` - + The time when the repository was last updated. + + +## Timeouts + +This resource provides the following +[Timeouts](/docs/configuration/resources.html#timeouts) configuration options: + +- `create` - Default is 4 minutes. +- `update` - Default is 4 minutes. +- `delete` - Default is 4 minutes. + +## Import + +Occurrence can be imported using any of these accepted formats: + +``` +$ terraform import google_container_analysis_occurrence.default projects/{{project}}/occurrences/{{name}} +$ terraform import google_container_analysis_occurrence.default {{project}}/{{name}} +$ terraform import google_container_analysis_occurrence.default {{name}} +``` + +-> If you're importing a resource with beta features, make sure to include `-provider=google-beta` +as an argument so that Terraform uses the correct provider to import your resource. + +## User Project Overrides + +This resource supports [User Project Overrides](https://www.terraform.io/docs/providers/google/guides/provider_reference.html#user_project_override). diff --git a/website/google.erb b/website/google.erb index 7832e37d8a4..cb290d99901 100644 --- a/website/google.erb +++ b/website/google.erb @@ -1475,6 +1475,10 @@ google_container_analysis_note +
  • + google_container_analysis_occurrence +
  • +
  • google_container_registry