hashicorp / terraform-provider-google

Terraform Provider for Google Cloud Platform
https://registry.terraform.io/providers/hashicorp/google/latest/docs
Mozilla Public License 2.0
2.31k stars 1.73k forks source link

google_app_engine_standard_app_version.handlers permadiff #13766

Open f0o opened 1 year ago

f0o commented 1 year ago

Community Note

Terraform Version

Terraform v1.3.5 on linux_amd64

Affected Resource(s)

Terraform Configuration Files

resource "google_app_engine_standard_app_version" "gae" {
  version_id       = local.git_hash_short
  service          = "foobar"
  runtime          = "python38"
  threadsafe       = true
  instance_class   = "F1"
  inbound_services = ["INBOUND_SERVICE_WARMUP"]

  entrypoint {
    shell = "true"
  }

  deployment {
    zip {
      source_url = "https://storage.googleapis.com/somewhere/over/the/rainbow.zip"
    }
  }

  handlers {
    auth_fail_action = "AUTH_FAIL_ACTION_REDIRECT"
    login            = "LOGIN_OPTIONAL"
    security_level   = "SECURE_ALWAYS"
    url_regex        = "/foobar/(.*\\.(ico|png|jpg|jpeg|gif|svg|txt|xml|js|map|css|eot|woff|woff2))$"

    static_files {
      application_readable  = false
      expiration            = "604800s"
      http_headers          = {}
      path                  = "\\1"
      require_matching_file = false
      upload_path_regex     = ".*\\.(ico|png|jpg|jpeg|gif|svg|txt|xml|js|map|css|eot|woff|woff2)$"
    }
  }

  handlers {
    auth_fail_action = "AUTH_FAIL_ACTION_REDIRECT"
    login            = "LOGIN_OPTIONAL"
    security_level   = "SECURE_ALWAYS"
    url_regex        = "/foobar.*"

    static_files {
      application_readable  = false
      expiration            = "0s"
      http_headers          = {}
      path                  = "index.html"
      require_matching_file = false
      upload_path_regex     = "index.html"
    }
  }

  handlers {
    auth_fail_action = "AUTH_FAIL_ACTION_REDIRECT"
    login            = "LOGIN_OPTIONAL"
    security_level   = "SECURE_OPTIONAL"
    url_regex        = ".*"

    script {
      script_path = "auto"
    }
  }

  noop_on_destroy = true
  delete_service_on_destroy = true

  lifecycle {
    create_before_destroy = true
  }
}

Debug Output

Panic Output

Expected Behavior

Repeated plan/apply should not cause a permadiff of a handler being removed

Actual Behavior

  # google_app_engine_standard_app_version.gae will be updated in-place
  ~ resource "google_app_engine_standard_app_version" "gae" {
        id                        = "xyz"
        name                      = "xyz"
        # (11 unchanged attributes hidden)

      - handlers {
          - auth_fail_action = "AUTH_FAIL_ACTION_REDIRECT" -> null
          - login            = "LOGIN_OPTIONAL" -> null
          - security_level   = "SECURE_OPTIONAL" -> null
          - url_regex        = ".*" -> null

          - script {
              - script_path = "auto" -> null
            }
        }

        # (5 unchanged blocks hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

Steps to Reproduce

  1. terraform apply
  2. terraform apply notice the permadiff

Important Factoids

View Config on GAE shows an added handler entry that is not defined by tf:

handlers:
  - url: >-
      /foobar/(.*\.(ico|png|jpg|jpeg|gif|svg|txt|xml|js|map|css|eot|woff|woff2))$
    secure: always
    expiration: '604800000'
    static_files: \1
    upload: .*\.(ico|png|jpg|jpeg|gif|svg|txt|xml|js|map|css|eot|woff|woff2)$
  - url: /foobar.*
    secure: always
    static_files: index.html
    upload: index.html
  - url: .*
    script: auto
  - url: .*
    script: auto

References

b/327681761

edwardmedia commented 1 year ago

@f0o can you share the debug log that contains the api requests and responses?

f0o commented 1 year ago

@edwardmedia

it POSTS:

POST /v1/apps/redacted/services/foo/versions?alt=json 
...
 "handlers": [
  {
   "authFailAction": "AUTH_FAIL_ACTION_REDIRECT",
   "login": "LOGIN_OPTIONAL",
   "securityLevel": "SECURE_ALWAYS",
   "staticFiles": {
    "expiration": "604800s",
    "path": "\\1",
    "uploadPathRegex": ".*\\.(ico|png|jpg|jpeg|gif|svg|txt|xml|js|map|css|eot|woff|woff2)$"
   },
   "urlRegex": "/foo/(.*\\.(ico|png|jpg|jpeg|gif|svg|txt|xml|js|map|css|eot|woff|woff2))$"
  },
  {
   "authFailAction": "AUTH_FAIL_ACTION_REDIRECT",
   "login": "LOGIN_OPTIONAL",
   "securityLevel": "SECURE_ALWAYS",
   "staticFiles": {
    "expiration": "0s",
    "path": "index.html",
    "uploadPathRegex": "index.html"
   },
   "urlRegex": "/foo.*"
  }
 ],

but it GETs:

GET /v1/apps/redacted/services/foo/versions/4d86979?alt=json&view=FULL
....
  "handlers": [
    {
      "urlRegex": "/foo/(.*\\.(ico|png|jpg|jpeg|gif|svg|txt|xml|js|map|css|eot|woff|woff2))$",
      "staticFiles": {
        "path": "\\1",
        "uploadPathRegex": ".*\\.(ico|png|jpg|jpeg|gif|svg|txt|xml|js|map|css|eot|woff|woff2)$",
        "expiration": "604800s"
      },                                                                                                                                                                                                                  "securityLevel": "SECURE_ALWAYS",
      "login": "LOGIN_OPTIONAL",
      "authFailAction": "AUTH_FAIL_ACTION_REDIRECT"                                                                                                                                                                     },
    {
      "urlRegex": "/foo.*",
      "staticFiles": {
        "path": "index.html",
        "uploadPathRegex": "index.html",
        "expiration": "0s"
      },
      "securityLevel": "SECURE_ALWAYS",
      "login": "LOGIN_OPTIONAL",
      "authFailAction": "AUTH_FAIL_ACTION_REDIRECT"
    },
    {
      "urlRegex": ".*",
      "script": {
        "scriptPath": "auto"
      },
      "securityLevel": "SECURE_OPTIONAL",
      "login": "LOGIN_OPTIONAL",
      "authFailAction": "AUTH_FAIL_ACTION_REDIRECT"
    }
  ],

It seems that GCP adds that .* handler automagically. It also adds it if I specify an identical one in TF. This results in a permadiff since there is always a handler added

f0o commented 1 year ago

Any news on this? It's quite annoying to have this permadiff

hao-nan-li commented 1 year ago

I can take a look into this.

hao-nan-li commented 1 year ago

Looks like you have 3 handlers in your TF config. Which handler did you remove and what did you do in order to remove it?

f0o commented 1 year ago

I removed prior to the logs:

  handlers {
    auth_fail_action = "AUTH_FAIL_ACTION_REDIRECT"
    login            = "LOGIN_OPTIONAL"
    security_level   = "SECURE_OPTIONAL"
    url_regex        = ".*"

    script {
      script_path = "auto"
    }
  }

However the logs were generated on entirely new appengine app with no prior state or creation, the permadiff and initial examples were from an imported resource so I couldnt modify those. Instead I created a new resource with:

  handlers {
    auth_fail_action = "AUTH_FAIL_ACTION_REDIRECT"
    login            = "LOGIN_OPTIONAL"
    security_level   = "SECURE_ALWAYS"
    url_regex        = "/redacted/(.*\\.(ico|png|jpg|jpeg|gif|svg|txt|xml|js|map|css|eot|woff|woff2))$"

    static_files {
      application_readable  = false
      expiration            = "604800s"
      http_headers          = {}
      path                  = "\\1"
      require_matching_file = false
      upload_path_regex     = ".*\\.(ico|png|jpg|jpeg|gif|svg|txt|xml|js|map|css|eot|woff|woff2)$"
    }
  }

  handlers {
    auth_fail_action = "AUTH_FAIL_ACTION_REDIRECT"
    login            = "LOGIN_OPTIONAL"
    security_level   = "SECURE_ALWAYS"
    url_regex        = "/redacted.*"

    static_files {
      application_readable  = false
      expiration            = "0s"
      http_headers          = {}
      path                  = "index.html"
      require_matching_file = false
      upload_path_regex     = "index.html"
    }
  }

which resulted in the log-lines mentioned in https://github.com/hashicorp/terraform-provider-google/issues/13766#issuecomment-1439787856

hao-nan-li commented 1 year ago

Basically you removed the handler with url_regex=.* from your TF config. Then you created a new resource. Then for some reason the handler with url_regex=.* is returned from the API.

https://cloud.google.com/appengine/docs/admin-api/reference/rest/v1/apps.services.versions#UrlMap as reference.

I don't see in the doc above that handler is immutable or anything. I may want to look at the following if possible: 1) Take out another handler and re-create just to see if something goes wrong with TF plan/apply. 2) Validate if the API default behaviour is to have that url_regex=.* handler (Although I did not find it in the doc). 3) Reach out to the app engine team to confirm the above.

f0o commented 1 year ago

Not really - even with the original handlers set it would return an additional .* handler - by the time I compiled the logs I just nuked everything and started over again, hence the seemingly missing .* handler.

I can paste you the full resource and logs in a moment

f0o commented 1 year ago
resource "google_app_engine_standard_app_version" "gae" {
  version_id       = local.git_hash_short
  service          = "tf-issue-13766"
  runtime          = "python38"
  threadsafe       = true
  instance_class   = "F1"

  entrypoint {
    shell = "true"
  }

  deployment {
    zip {
      source_url = "https://storage.googleapis.com/${data.terraform_remote_state.project.outputs.assets_bucket.name}/${google_storage_bucket_object.assets.output_name}"
    }
  }

  handlers {
    auth_fail_action = "AUTH_FAIL_ACTION_REDIRECT"
    login            = "LOGIN_OPTIONAL"
    security_level   = "SECURE_ALWAYS"
    url_regex        = "/something/(.*\\.(ico|png|jpg|jpeg|gif|svg|txt|xml|js|map|css|eot|woff|woff2))$"

    static_files {
      application_readable  = false
      expiration            = "604800s"
      http_headers          = {}
      path                  = "\\1"
      require_matching_file = false
      upload_path_regex     = ".*\\.(ico|png|jpg|jpeg|gif|svg|txt|xml|js|map|css|eot|woff|woff2)$"
    }
  }

  handlers {
    auth_fail_action = "AUTH_FAIL_ACTION_REDIRECT"
    login            = "LOGIN_OPTIONAL"
    security_level   = "SECURE_ALWAYS"
    url_regex        = "/something.*"

    static_files {
      application_readable  = false
      expiration            = "0s"
      http_headers          = {}
      path                  = "index.html"
      require_matching_file = false
      upload_path_regex     = "index.html"
    }
  }

  // We will clean up old versions outside of tf
  noop_on_destroy = true

  // If we deleted all versions, we also delete the service
  delete_service_on_destroy = true

  lifecycle {
    create_before_destroy = true
  }
}

resource "google_app_engine_service_split_traffic" "gae" {
  service         = google_app_engine_standard_app_version.gae.service
  migrate_traffic = true

  split {
    shard_by    = "IP"
    allocations = { (google_app_engine_standard_app_version.gae.version_id) = 1 }
  }
}

The Plan:

  # google_app_engine_service_split_traffic.gae will be created
  + resource "google_app_engine_service_split_traffic" "gae" {
      + id              = (known after apply)
      + migrate_traffic = true
      + project         = (known after apply)
      + service         = "tf-issue-13766"

      + split {
          + allocations = {
              + "4d86979" = "1"
            }
          + shard_by    = "IP"
        }
    }

  # google_app_engine_standard_app_version.gae will be created
  + resource "google_app_engine_standard_app_version" "gae" {
      + delete_service_on_destroy = true
      + id                        = (known after apply)
      + instance_class            = "F1"
      + name                      = (known after apply)
      + noop_on_destroy           = true
      + project                   = (known after apply)
      + runtime                   = "python38"
      + service                   = "tf-issue-13766"
      + service_account           = (known after apply)
      + threadsafe                = true
      + version_id                = "4d86979"

      + deployment {

          + zip {
              + source_url = (known after apply)
            }
        }

      + entrypoint {
          + shell = "true"
        }

      + handlers {
          + auth_fail_action = "AUTH_FAIL_ACTION_REDIRECT"
          + login            = "LOGIN_OPTIONAL"
          + security_level   = "SECURE_ALWAYS"
          + url_regex        = "/something/(.*\\.(ico|png|jpg|jpeg|gif|svg|txt|xml|js|map|css|eot|woff|woff2))$"

          + static_files {
              + application_readable  = false
              + expiration            = "604800s"
              + path                  = "\\1"
              + require_matching_file = false
              + upload_path_regex     = ".*\\.(ico|png|jpg|jpeg|gif|svg|txt|xml|js|map|css|eot|woff|woff2)$"
            }
        }
      + handlers {
          + auth_fail_action = "AUTH_FAIL_ACTION_REDIRECT"
          + login            = "LOGIN_OPTIONAL"
          + security_level   = "SECURE_ALWAYS"
          + url_regex        = "/something.*"

          + static_files {
              + application_readable  = false
              + expiration            = "0s"
              + path                  = "index.html"
              + require_matching_file = false
              + upload_path_regex     = "index.html"
            }
        }
    }

The Apply Log (snipped to POST for creation and GET for validation):

...
google_app_engine_standard_app_version.gae: Creating...
2023-03-08T08:54:39.479+0100 [INFO]  Starting apply for google_app_engine_standard_app_version.gae
2023-03-08T08:54:39.480+0100 [DEBUG] google_app_engine_standard_app_version.gae: applying the planned Create change
2023-03-08T08:54:39.482+0100 [INFO]  provider.terraform-provider-google_v4.53.1_x5: 2023/03/08 08:54:39 [DEBUG] Locking "apps/redacted_project": timestamp=2023-03-08T08:54:39.482+0100
2023-03-08T08:54:39.482+0100 [INFO]  provider.terraform-provider-google_v4.53.1_x5: 2023/03/08 08:54:39 [DEBUG] Locked "apps/redacted_project": timestamp=2023-03-08T08:54:39.482+0100
2023-03-08T08:54:39.482+0100 [INFO]  provider.terraform-provider-google_v4.53.1_x5: 2023/03/08 08:54:39 [DEBUG] Creating new StandardAppVersion: map[string]interface {}{"deployment":map[string]interface {}{"zip":
map[string]interface {}{"sourceUrl":"https://storage.googleapis.com/redacted_bucket/gae/tf-issue-13766/4d86979.zip"}}, "entrypoint":map[string]interface {}{"shell":"true"}
, "handlers":[]interface {}{map[string]interface {}{"authFailAction":"AUTH_FAIL_ACTION_REDIRECT", "login":"LOGIN_OPTIONAL", "securityLevel":"SECURE_ALWAYS", "staticFiles":map[string]interface {}{"expiration":"604
800s", "path":"\\1", "uploadPathRegex":".*\\.(ico|png|jpg|jpeg|gif|svg|txt|xml|js|map|css|eot|woff|woff2)$"}, "urlRegex":"/something/(.*\\.(ico|png|jpg|jpeg|gif|svg|txt|xml|js|map|css|eot|woff|woff2))$"}, map[str
ing]interface {}{"authFailAction":"AUTH_FAIL_ACTION_REDIRECT", "login":"LOGIN_OPTIONAL", "securityLevel":"SECURE_ALWAYS", "staticFiles":map[string]interface {}{"expiration":"0s", "path":"index.html", "uploadPathR
egex":"index.html"}, "urlRegex":"/something.*"}}, "id":"4d86979", "instanceClass":"F1", "runtime":"python38", "threadsafe":true}: timestamp=2023-03-08T08:54:39.482+0100
2023-03-08T08:54:39.482+0100 [INFO]  provider.terraform-provider-google_v4.53.1_x5: 2023/03/08 08:54:39 [DEBUG] Waiting for state to become: [success]: timestamp=2023-03-08T08:54:39.482+0100
2023-03-08T08:54:39.482+0100 [INFO]  provider.terraform-provider-google_v4.53.1_x5: 2023/03/08 08:54:39 [DEBUG] Retry Transport: starting RoundTrip retry loop: timestamp=2023-03-08T08:54:39.482+0100
2023-03-08T08:54:39.482+0100 [INFO]  provider.terraform-provider-google_v4.53.1_x5: 2023/03/08 08:54:39 [DEBUG] Retry Transport: request attempt 0: timestamp=2023-03-08T08:54:39.482+0100
2023-03-08T08:54:39.482+0100 [INFO]  provider.terraform-provider-google_v4.53.1_x5: 2023/03/08 08:54:39 [DEBUG] Google API Request Details:
---[ REQUEST ]---------------------------------------
POST /v1/apps/redacted_project/services/tf-issue-13766/versions?alt=json HTTP/1.1
Host: appengine.googleapis.com
User-Agent: Terraform/1.3.5 (+https://www.terraform.io) Terraform-Plugin-SDK/2.10.1 terraform-provider-google/4.53.1
Content-Length: 827
Content-Type: application/json
X-Goog-User-Project: redacted_project
Accept-Encoding: gzip

{
 "deployment": {
  "zip": {
   "sourceUrl": "https://storage.googleapis.com/redacted_bucket/gae/tf-issue-13766/4d86979.zip"
  }
 },
 "entrypoint": {
  "shell": "true"
 },
 "handlers": [
  {
   "authFailAction": "AUTH_FAIL_ACTION_REDIRECT",
   "login": "LOGIN_OPTIONAL",
   "securityLevel": "SECURE_ALWAYS",
   "staticFiles": {
    "expiration": "604800s",
    "path": "\\1",
    "uploadPathRegex": ".*\\.(ico|png|jpg|jpeg|gif|svg|txt|xml|js|map|css|eot|woff|woff2)$"
   },
   "urlRegex": "/something/(.*\\.(ico|png|jpg|jpeg|gif|svg|txt|xml|js|map|css|eot|woff|woff2))$"
  },
  {
   "authFailAction": "AUTH_FAIL_ACTION_REDIRECT",
   "login": "LOGIN_OPTIONAL",
   "securityLevel": "SECURE_ALWAYS",
   "staticFiles": {
    "expiration": "0s",
    "path": "index.html",
    "uploadPathRegex": "index.html"
   },
   "urlRegex": "/something.*"
  }
 ],
 "id": "4d86979",
 "instanceClass": "F1",
 "runtime": "python38",
 "threadsafe": true
}

-----------------------------------------------------: timestamp=2023-03-08T08:54:39.482+0100
2023-03-08T08:54:39.904+0100 [INFO]  provider.terraform-provider-google_v4.53.1_x5: 2023/03/08 08:54:39 [DEBUG] Google API Response Details:
---[ RESPONSE ]--------------------------------------
HTTP/2.0 200 OK
Alt-Svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
Cache-Control: private
Content-Type: application/json; charset=UTF-8
Date: Wed, 08 Mar 2023 07:54:39 GMT
Server: ESF
Vary: Origin
Vary: X-Origin
Vary: Referer
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 0

{
  "name": "apps/redacted_project/operations/bcbb3670-f6cf-442c-8f60-55008cc0bfd5",
  "metadata": {
    "@type": "type.googleapis.com/google.appengine.v1.OperationMetadataV1",
    "method": "google.appengine.v1.Versions.CreateVersion",
    "insertTime": "2023-03-08T07:54:39.843Z",
    "user": "redacted_account",
    "target": "apps/redacted_project/services/tf-issue-13766/versions/4d86979"
  }
}

-----------------------------------------------------: timestamp=2023-03-08T08:54:39.904+0100
...
2023-03-08T08:55:41.756+0100 [INFO]  provider.terraform-provider-google_v4.53.1_x5: 2023/03/08 08:55:41 [DEBUG] Google API Request Details:
---[ REQUEST ]---------------------------------------
GET /v1/apps/redacted_project/services/tf-issue-13766/versions/4d86979?alt=json&view=FULL HTTP/1.1
Host: appengine.googleapis.com
User-Agent: Terraform/1.3.5 (+https://www.terraform.io) Terraform-Plugin-SDK/2.10.1 terraform-provider-google/4.53.1
Content-Type: application/json
X-Goog-User-Project: redacted_project
Accept-Encoding: gzip

-----------------------------------------------------: timestamp=2023-03-08T08:55:41.756+0100
2023-03-08T08:55:41.866+0100 [INFO]  provider.terraform-provider-google_v4.53.1_x5: 2023/03/08 08:55:41 [DEBUG] Google API Response Details:
---[ RESPONSE ]--------------------------------------
HTTP/2.0 200 OK
Alt-Svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
Cache-Control: private
Content-Type: application/json; charset=UTF-8
Date: Wed, 08 Mar 2023 07:55:41 GMT
Server: ESF
Vary: Origin
Vary: X-Origin
Vary: Referer
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 0

{
  "name": "apps/redacted_project/services/tf-issue-13766/versions/4d86979",
  "id": "4d86979",
  "instanceClass": "F1",
  "network": {},
  "runtime": "python38",
  "threadsafe": true,
  "env": "standard",
  "servingStatus": "SERVING",
  "createdBy": "redacted_account",
  "createTime": "2023-03-08T07:55:29Z",
  "diskUsageBytes": "4440348",
  "handlers": [
    {
      "urlRegex": "/something/(.*\\.(ico|png|jpg|jpeg|gif|svg|txt|xml|js|map|css|eot|woff|woff2))$",
      "staticFiles": {
        "path": "\\1",
        "uploadPathRegex": ".*\\.(ico|png|jpg|jpeg|gif|svg|txt|xml|js|map|css|eot|woff|woff2)$",
        "expiration": "604800s"
      },
      "securityLevel": "SECURE_ALWAYS",
      "login": "LOGIN_OPTIONAL",
      "authFailAction": "AUTH_FAIL_ACTION_REDIRECT"
    },
    {
      "urlRegex": "/something.*",
      "staticFiles": {
        "path": "index.html",
        "uploadPathRegex": "index.html",
        "expiration": "0s"
      },
      "securityLevel": "SECURE_ALWAYS",
      "login": "LOGIN_OPTIONAL",
      "authFailAction": "AUTH_FAIL_ACTION_REDIRECT"
    },
    {
      "urlRegex": ".*",
      "script": {
        "scriptPath": "auto"
      },
      "securityLevel": "SECURE_OPTIONAL",
      "login": "LOGIN_OPTIONAL",
      "authFailAction": "AUTH_FAIL_ACTION_REDIRECT"
    }
  ],
  "deployment": {
...
  },
  "versionUrl": "https://4d86979-dot-tf-issue-13766-dot-redacted_project.appspot.com",
  "runtimeChannel": "default",
  "serviceAccount": "redacted_project@appspot.gserviceaccount.com"
}

-----------------------------------------------------: timestamp=2023-03-08T08:55:41.865+0100
2023-03-08T08:55:41.866+0100 [INFO]  provider.terraform-provider-google_v4.53.1_x5: 2023/03/08 08:55:41 [DEBUG] Retry Transport: Stopping retries, last request was successful: timestamp=2023-03-08T08:55:41.866+0100
2023-03-08T08:55:41.866+0100 [INFO]  provider.terraform-provider-google_v4.53.1_x5: 2023/03/08 08:55:41 [DEBUG] Retry Transport: Returning after 1 attempts: timestamp=2023-03-08T08:55:41.866+0100
2023-03-08T08:55:41.867+0100 [INFO]  provider.terraform-provider-google_v4.53.1_x5: 2023/03/08 08:55:41 [DEBUG] Unlocking "apps/redacted_project": timestamp=2023-03-08T08:55:41.867+0100
2023-03-08T08:55:41.867+0100 [INFO]  provider.terraform-provider-google_v4.53.1_x5: 2023/03/08 08:55:41 [DEBUG] Unlocked "apps/redacted_project": timestamp=2023-03-08T08:55:41.867+0100
2023-03-08T08:55:41.869+0100 [WARN]  Provider "provider[\"registry.terraform.io/hashicorp/google\"]" produced an unexpected new value for google_app_engine_standard_app_version.gae, but we are tolerating it because it is using the legacy plugin SDK.
    The following problems may be the cause of any confusing errors from downstream operations:
      - .runtime_api_version: was null, but now cty.StringVal("")
      - .app_engine_apis: was null, but now cty.False
      - .deployment[0].zip[0].files_count: was null, but now cty.NumberIntVal(0)
      - .handlers: block count changed from 2 to 3
google_app_engine_standard_app_version.gae: Creation complete after 1m3s [id=apps/redacted_project/services/tf-issue-13766/versions/4d86979]

As you can see here the handlers that were POST'ed to create the resource match the terraform but once the creation completes it actually responds with one additional handler:

    {
      "urlRegex": ".*",
      "script": {
        "scriptPath": "auto"
      },
      "securityLevel": "SECURE_OPTIONAL",
      "login": "LOGIN_OPTIONAL",
      "authFailAction": "AUTH_FAIL_ACTION_REDIRECT"
    }

This is yet again a whole new app-engine service, I have not re-used any names or assets. This is a blank state.

Immediately issuing a terraform plan will respond:

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # google_app_engine_standard_app_version.gae will be updated in-place
  ~ resource "google_app_engine_standard_app_version" "gae" {
        id                        = "apps/redacted_project/services/tf-issue-13766/versions/4d86979"
        name                      = "apps/redacted_project/services/tf-issue-13766/versions/4d86979"
        # (11 unchanged attributes hidden)

      - handlers {
          - auth_fail_action = "AUTH_FAIL_ACTION_REDIRECT" -> null
          - login            = "LOGIN_OPTIONAL" -> null
          - security_level   = "SECURE_OPTIONAL" -> null
          - url_regex        = ".*" -> null

          - script {
              - script_path = "auto" -> null
            }
        }

        # (4 unchanged blocks hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

But nothing was changed - even after terraform apply to get it removed a new plan will yield the exact same change again. It's like it's permanently added by GAE

hao-nan-li commented 1 year ago

I suspect there is a problem with the app engine API.

Is this issue still reproducible?

f0o commented 1 year ago

@hao-nan-li Yes

Terraform will perform the following actions:

  # google_app_engine_standard_app_version.gae will be updated in-place
  ~ resource "google_app_engine_standard_app_version" "gae" {
        id                        = "apps/redacted/services/tf-issue-13766-2/versions/4d86979"
        name                      = "apps/redacted/services/tf-issue-13766-2/versions/4d86979"
        # (11 unchanged attributes hidden)

      - handlers {
          - auth_fail_action = "AUTH_FAIL_ACTION_REDIRECT" -> null
          - login            = "LOGIN_OPTIONAL" -> null
          - security_level   = "SECURE_OPTIONAL" -> null
          - url_regex        = ".*" -> null

          - script {
              - script_path = "auto" -> null
            }
        }

        # (4 unchanged blocks hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.
f0o commented 1 year ago

Any news on this?

alyson-ferreira-trimble commented 1 year ago

Hi, I'm having the same issue.

I tried adding the exact same handler at the end of the handlers list (blocks). It magically added another one.

Running terraform state show on it shows the additional handler with the same configuration.

# module.frontend.google_app_engine_standard_app_version.app_engine_version:
resource "google_app_engine_standard_app_version" "app_engine_version" {
    app_engine_apis           = false
    delete_service_on_destroy = false
    id                        = "apps/<redacted-project>/services/default/versions/current"
    inbound_services          = []
    instance_class            = "F1"
    name                      = "apps/<redacted-project>/services/default/versions/current"
    noop_on_destroy           = true
    project                   = "<redacted-project>"
    runtime                   = "nodejs18"
    service                   = "default"
    service_account           = "<redacted>@<redacted-project>.iam.gserviceaccount.com"
    version_id                = "current"

    deployment {

        zip {
            files_count = 0
            source_url  = "https://storage.googleapis.com/<redacted>.zip"
        }
    }

    entrypoint {
        shell = "echo 'running SPA'"
    }

    handlers {
        auth_fail_action = "AUTH_FAIL_ACTION_REDIRECT"
        login            = "LOGIN_OPTIONAL"
        security_level   = "SECURE_ALWAYS"
        url_regex        = "/favicon\\.ico"

        static_files {
            application_readable  = false
            expiration            = "0s"
            http_headers          = {}
            path                  = "favicon.ico"
            require_matching_file = false
            upload_path_regex     = "favicon\\.ico"
        }
    }
    handlers {
        auth_fail_action = "AUTH_FAIL_ACTION_REDIRECT"
        login            = "LOGIN_OPTIONAL"
        security_level   = "SECURE_ALWAYS"
        url_regex        = "/static/(.*)"

        static_files {
            application_readable  = false
            expiration            = "0s"
            http_headers          = {}
            path                  = "static/\\1"
            require_matching_file = false
            upload_path_regex     = "static/.*"
        }
    }
    handlers {
        auth_fail_action = "AUTH_FAIL_ACTION_REDIRECT"
        login            = "LOGIN_OPTIONAL"
        security_level   = "SECURE_ALWAYS"
        url_regex        = ".*"

        static_files {
            application_readable  = false
            expiration            = "0s"
            http_headers          = {}
            path                  = "index.html"
            require_matching_file = false
            upload_path_regex     = "index.html"
        }
    }
    handlers {
        auth_fail_action = "AUTH_FAIL_ACTION_REDIRECT"
        login            = "LOGIN_OPTIONAL"
        security_level   = "SECURE_OPTIONAL"
        url_regex        = ".*"

        script {
            script_path = "auto"
        }
    }
    handlers {
        auth_fail_action = "AUTH_FAIL_ACTION_REDIRECT"
        login            = "LOGIN_OPTIONAL"
        security_level   = "SECURE_OPTIONAL"
        url_regex        = ".*"

        script {
            script_path = "auto"
        }
    }
}
f0o commented 1 year ago

@alyson-ferreira-trimble We have classified this as "cosmetic" issue internally because it doesnt really impact your setup but it makes reading tf plan a nuisance of hell. I'm now training our staff to mentally "ignore" those changes... Which by all means is VERY BAD PRACTICE to blindly ignore changes to a whole resource.

I do hope this will be fixed sooner than later.

melinath commented 7 months ago

I can reproduce this permadiff. Guidance on fixing permadiffs: https://googlecloudplatform.github.io/magic-modules/develop/permadiff/

Here's a config that reproduces the failure (using the hello-world-flask app from the provider tests) - fill out the billing account & org id and apply twice to see the permadiff:

resource "random_string" "suffix" {
  length  = 4
  upper   = false
  special = false
}

resource "google_project" "my_project" {
  name = "tf-test-appeng-std${random_string.suffix.result}"
  project_id = "tf-test-appeng-std${random_string.suffix.result}"
  billing_account = "REPLACE_BILLING_ACCOUNT"
  org_id = "REPLACE_ORG_ID"
}

resource "google_app_engine_application" "app" {
  project = google_project.my_project.project_id
  location_id = "us-central"
}

resource "google_app_engine_standard_app_version" "foo" {
  project = google_project.my_project.project_id
  version_id = "v1"
  service    = "default"
  runtime    = "python38"

  entrypoint {
    shell = "gunicorn -b :$PORT main:app"
  }

  deployment {
    files {
      name = "main.py"
      source_url = "https://storage.googleapis.com/${google_storage_bucket.bucket.name}/${google_storage_bucket_object.main.name}"
    }

    files {
      name = "requirements.txt"
      source_url = "https://storage.googleapis.com/${google_storage_bucket.bucket.name}/${google_storage_bucket_object.requirements.name}"
    }
  }

  inbound_services = ["INBOUND_SERVICE_WARMUP", "INBOUND_SERVICE_MAIL"]

  env_variables = {
    port = "8000"
  }

  instance_class = "F2"

  automatic_scaling {
    max_concurrent_requests = 10
    min_idle_instances = 1
    max_idle_instances = 3
    min_pending_latency = "1s"
    max_pending_latency = "5s"
    standard_scheduler_settings {
      target_cpu_utilization = 0.5
      target_throughput_utilization = 0.75
      min_instances = 2
      max_instances = 10
    }
  }

  noop_on_destroy = true

  handlers {
    auth_fail_action = "AUTH_FAIL_ACTION_REDIRECT"
    login            = "LOGIN_OPTIONAL"
    security_level   = "SECURE_ALWAYS"
    url_regex        = "/foobar/(.*\\.(ico|png|jpg|jpeg|gif|svg|txt|xml|js|map|css|eot|woff|woff2))$"

    static_files {
      application_readable  = false
      expiration            = "604800s"
      http_headers          = {}
      path                  = "\\1"
      require_matching_file = false
      upload_path_regex     = ".*\\.(ico|png|jpg|jpeg|gif|svg|txt|xml|js|map|css|eot|woff|woff2)$"
    }
  }

  handlers {
    auth_fail_action = "AUTH_FAIL_ACTION_REDIRECT"
    login            = "LOGIN_OPTIONAL"
    security_level   = "SECURE_ALWAYS"
    url_regex        = "/foobar.*"

    static_files {
      application_readable  = false
      expiration            = "0s"
      http_headers          = {}
      path                  = "index.html"
      require_matching_file = false
      upload_path_regex     = "index.html"
    }
  }

  handlers {
    auth_fail_action = "AUTH_FAIL_ACTION_REDIRECT"
    login            = "LOGIN_OPTIONAL"
    security_level   = "SECURE_OPTIONAL"
    url_regex        = ".*"

    script {
      script_path = "auto"
    }
  }
}

resource "google_storage_bucket" "bucket" {
  project = google_project.my_project.project_id
  name     = "tf-test-${random_string.suffix.result}-standard-ae-bucket"
  location = "US"
  force_destroy = true
}

resource "google_storage_bucket_object" "requirements" {
  name   = "requirements.txt"
  bucket = google_storage_bucket.bucket.name
  source = "./hello-world-flask/requirements.txt"
}

resource "google_storage_bucket_object" "main" {
  name   = "main.py"
  bucket = google_storage_bucket.bucket.name
  source = "./hello-world-flask/main.py"
}