terraform-provider-gitlab icon indicating copy to clipboard operation
terraform-provider-gitlab copied to clipboard

Attempts to change path of project if container registry tags exist succeed in Terraform, but this action is not allowed by Gitlab

Open twalker1998 opened this issue 1 year ago • 9 comments

GitLab Provider version

3.16.1

GitLab version

15.3.0-pre

Terraform version

1.2.5

Relevant Terraform Configuration

module "project_obs_ci_images" {
  source  = "app.terraform.io/oklahoma-biological-survey/project/gitlab"
  version = "~>2.0.0"

  name        = "OBS CI Images"
  path        = "obs-ci-images"
  description = "Oklahoma Biological Survey CI Images"
  group_id    = module.group_oklahoma_biological_survey.group_id
}

Relevant log output

Terraform will perform the following actions:

  # module.project_obs_ci_images.gitlab_project.this will be updated in-place
  ~ resource "gitlab_project" "this" {
      ~ description                                      = "Oklahoma Biological Survey CI Build Images" -> "Oklahoma Biological Survey CI Images"
      ~ http_url_to_repo                                 = "https://gitlab.com/oklahoma-biological-survey/build-images.git" -> (known after apply)
        id                                               = "<project_id>"
      ~ name                                             = "Build Images" -> "OBS CI Images"
      ~ path                                             = "build-images" -> "obs-ci-images"
      ~ path_with_namespace                              = "oklahoma-biological-survey/build-images" -> (known after apply)
      ~ ssh_url_to_repo                                  = "[email protected]:oklahoma-biological-survey/build-images.git" -> (known after apply)
        tags                                             = []
      ~ web_url                                          = "https://gitlab.com/oklahoma-biological-survey/build-images" -> (known after apply)
        # (55 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

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

Description

I've changed the name of an existing project that is managed with Terraform and has existing container registry tags. The original configuration is listed below (updated configuration is in the Relevant Terraform Configuration section):

module "project_build_images" {
  source  = "app.terraform.io/oklahoma-biological-survey/project/gitlab"
  version = "~>2.0.0"

  name        = "Build Images"
  path        = "build-images"
  description = "Oklahoma Biological Survey CI Build Images"
  group_id    = module.group_oklahoma_biological_survey.group_id
}

I moved these resources in the state (considering I renamed the module) prior to running Terraform. The plan continues to show the output above, even after running the apply multiple times. The apply is always successful (as shown below), but no changes are made to the state file and these changes are not reflected in Gitlab (SaaS offering).

After trying to make these changes manually in the UI (luckily I have Owner level access to my Gitlab instance), I receive this error message when updating the project's path:

Cannot rename project because it contains container registry tags!

This seems like a limitation of the Gitlab API, but I'm thinking that the Terraform provider should also show this error message, as it seems like the changes were made, but they certainly were not.

Output after apply is ran:

module.project_obs_ci_images.gitlab_project.this: Modifying... [id=<project_id>]
module.project_obs_ci_images.gitlab_project.this: Modifications complete after 1s [id=<project_id>]

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

twalker1998 avatar Jul 26 '22 03:07 twalker1998

Yes, it should actually yield the same errors than the API!

Do you mind providing a standalone example using the gitlab_project resource directly?

timofurrer avatar Jul 26 '22 13:07 timofurrer

That was my thinking as well!

Here is the resource configuration within my module. The example I provided in the initial report was modifying the value for var.path (among others). Is this sufficient? Happy to provide more information if need be.

resource "gitlab_project" "this" {
  name                                             = var.name
  path                                             = var.path
  description                                      = var.description
  namespace_id                                     = var.group_id
  default_branch                                   = var.default_branch
  visibility_level                                 = "private"
  merge_requests_enabled                           = true
  remove_source_branch_after_merge                 = true
  initialize_with_readme                           = false
  request_access_enabled                           = false
  shared_runners_enabled                           = false
  archived                                         = var.archived
  approvals_before_merge                           = var.num_approvals
  tags                                             = null
  only_allow_merge_if_pipeline_succeeds            = var.merge_request_pipelines_enabled
  allow_merge_on_skipped_pipeline                  = var.merge_request_pipelines_enabled
  only_allow_merge_if_all_discussions_are_resolved = true
  build_git_strategy                               = "clone"
}

twalker1998 avatar Jul 26 '22 21:07 twalker1998

FYI, I created an indicative test for this (note - to run this test you need to make some changes to the docker-compose file and you need to add a hostfile entry on your localhost as well):


func TestAccGitlabProject_withContainer(t *testing.T) {
	var received gitlab.Project
	rInt := acctest.RandInt()

	resource.ParallelTest(t, resource.TestCase{
		ProviderFactories: providerFactories,
		CheckDestroy:      testAccCheckGitlabProjectDestroy,
		Steps: []resource.TestStep{
			{
				Config: fmt.Sprintf(`
					resource "gitlab_project" "this" {
						name = "foo-%d"
						visibility_level = "public"
					}`, rInt),
				Check: resource.ComposeTestCheckFunc(
					testAccCheckGitlabProjectExists("gitlab_project.this", &received),
					func(s *terraform.State) error {
						exec.Command("docker login -u root --password ACCTEST1234567890123 localhost:5050").Run()
						exec.Command("docker pull alpine").Run()
						path := fmt.Sprintf("localhost:5050/%s", received.PathWithNamespace)
						exec.Command(fmt.Sprintf("docker tag alpine %s", path)).Run()
						exec.Command(fmt.Sprintf("docker push %s", path)).Run()
						return nil
					},
				),
			},
			// Change Name
			{
				Config: fmt.Sprintf(`
				resource "gitlab_project" "this" {
					name = "bar-%d"
					visibility_level = "public"
				}`, rInt),
				Check: resource.ComposeTestCheckFunc(
					func(s *terraform.State) error {
                                                // here for debugging breakpoint purposes.
						log.Println("stuff")
						return nil
					},
				),
			},
			// Verify Import
			{
				ResourceName:      "gitlab_project.this",
				ImportState:       true,
				ImportStateVerify: true,
			},
		},
	})
}

I can confirm the test passes, and notably, it passes because it successfully renames the project while the container registry tag is present. Maybe this is failing because in addition to changing the project name, you're changing the namespace as well?

Before rename: image

After rename: image

PatrickRice-KSC avatar Jul 29 '22 18:07 PatrickRice-KSC

@PatrickRice-KSC can you change the path instead? AFAIK, the name is merely used to display a name. It may not even require a namespace change (as in the namespace_id.

timofurrer avatar Jul 30 '22 06:07 timofurrer

can you change the path instead? AFAIK, the name is merely used to display a name. It may not even require a namespace change (as in the namespace_id.

For sure - I'll give it a whirl here shortly.

PatrickRice-KSC avatar Jul 30 '22 22:07 PatrickRice-KSC

Yes, I was able to successfully rename the project from the UI with no issues, but the error occurred when I changed the path.

twalker1998 avatar Jul 30 '22 23:07 twalker1998

Here we go, updated test:

func TestAccGitlabProject_withContainer(t *testing.T) {
	var received gitlab.Project
	rInt := acctest.RandInt()

	resource.ParallelTest(t, resource.TestCase{
		ProviderFactories: providerFactories,
		CheckDestroy:      testAccCheckGitlabProjectDestroy,
		Steps: []resource.TestStep{
			{
				Config: fmt.Sprintf(`
					resource "gitlab_project" "this" {
						name = "foo-%d"
						path = "test"
						visibility_level = "public"
					}`, rInt),
				Check: resource.ComposeTestCheckFunc(
					testAccCheckGitlabProjectExists("gitlab_project.this", &received),
					func(s *terraform.State) error {
						exec.Command("/usr/bin/docker login -u root --password ACCTEST1234567890123 localhost:5050").Run()
						exec.Command("/usr/bin/docker pull alpine").Run()
						path := fmt.Sprintf("localhost:5050/%s", received.PathWithNamespace)
						exec.Command(fmt.Sprintf("/usr/bin/docker tag alpine %s", path)).Run()
						exec.Command(fmt.Sprintf("/usr/bin/docker push %s", path)).Run()
						return nil
					},
				),
			},
			// Change Name
			{
				Config: fmt.Sprintf(`
				resource "gitlab_project" "this" {
					name = "foo-%d"
					path = "test-change"
					visibility_level = "public"
				}`, rInt),
				Check: resource.ComposeTestCheckFunc(
					func(s *terraform.State) error {
						log.Println("stuff")
						return nil
					},
				),
			},
		},
	})
}

Result: test fails properly, with the path not changing in GitLab:

However, it's worth noting that based on the way the test fails, you can see it's not that the apply failed (which I think is what we'd expect here) - it's failing because the plan was not empty. I think that mirrors the behavior that @twalker1998 is talking that, where the apply should fail, and it doesn't.

--- FAIL: TestAccGitlabProject_withContainer (47.21s)
    resource_gitlab_project_test.go:30: Step 2/2 error: After applying this test step, the plan was not empty.
        stdout:
        
        
        An execution plan has been generated and is shown below.
        Resource actions are indicated with the following symbols:
          ~ update in-place
        
        Terraform will perform the following actions:
        
          # gitlab_project.this will be updated in-place
          ~ resource "gitlab_project" "this" {
                allow_merge_on_skipped_pipeline                  = false
                analytics_access_level                           = "enabled"
                approvals_before_merge                           = 0
                archived                                         = false
                auto_cancel_pending_pipelines                    = "enabled"
                auto_devops_deploy_strategy                      = "continuous"
                auto_devops_enabled                              = true
                autoclose_referenced_issues                      = true
                build_git_strategy                               = "fetch"
                build_timeout                                    = 3600
                builds_access_level                              = "enabled"
                ci_default_git_depth                             = 20
                ci_forward_deployment_enabled                    = true
                container_registry_access_level                  = "enabled"
                container_registry_enabled                       = true
                default_branch                                   = "main"
                emails_disabled                                  = false
                forking_access_level                             = "enabled"
              ~ http_url_to_repo                                 = "http://gitlab/root/test.git" -> (known after apply)
                id                                               = "13"
                issues_access_level                              = "enabled"
                issues_enabled                                   = true
                lfs_enabled                                      = true
                merge_method                                     = "merge"
                merge_pipelines_enabled                          = false
                merge_requests_access_level                      = "enabled"
                merge_requests_enabled                           = true
                merge_trains_enabled                             = false
                mirror                                           = false
                mirror_overwrites_diverged_branches              = false
                mirror_trigger_builds                            = false
                name                                             = "foo-1817053023237789349"
                namespace_id                                     = 1
                only_allow_merge_if_all_discussions_are_resolved = false
                only_allow_merge_if_pipeline_succeeds            = false
                only_mirror_protected_branches                   = false
                operations_access_level                          = "enabled"
                packages_enabled                                 = true
                pages_access_level                               = "private"
              ~ path                                             = "test" -> "test-change"
              ~ path_with_namespace                              = "root/test" -> (known after apply)
                pipelines_enabled                                = true
                printing_merge_request_link_enabled              = true
                public_builds                                    = false
                remove_source_branch_after_merge                 = false
                repository_access_level                          = "enabled"
                repository_storage                               = "default"
                request_access_enabled                           = true
                resolve_outdated_diff_discussions                = false
                runners_token                                    = (sensitive value)
                security_and_compliance_access_level             = "private"
                shared_runners_enabled                           = false
                snippets_access_level                            = "enabled"
                snippets_enabled                                 = true
                squash_option                                    = "default_off"
              ~ ssh_url_to_repo                                  = "git@gitlab:root/test.git" -> (known after apply)
                tags                                             = []
                topics                                           = []
                visibility_level                                 = "public"
              ~ web_url                                          = "http://gitlab/root/test" -> (known after apply)
                wiki_access_level                                = "enabled"
                wiki_enabled                                     = true
        
                container_expiration_policy {
                    cadence     = "1d"
                    enabled     = false
                    keep_n      = 10
                    next_run_at = "2022-08-01T02:58:07Z"
                    older_than  = "90d"
                }
            }
        
        Plan: 0 to add, 1 to change, 0 to destroy.
FAIL

PatrickRice-KSC avatar Jul 31 '22 03:07 PatrickRice-KSC

@twalker1998

This seems like a limitation of the Gitlab API, ...

Yes, it indeed seems like a limitation of the GitLab API. Running a curl command manually against a project with existing images in the container registry responds with a 200, but a null body.

..., but I'm thinking that the Terraform provider should also show this error message, as it seems like the changes were made, but they certainly were not.

Yes, it should and actually it would already if the GitLab API would return a non-success status code and would display the response body in the error message.

@twalker1998 do you mind creating the issue upstream?

timofurrer avatar Aug 05 '22 13:08 timofurrer

@timofurrer - WDYT about adding a StateChangeConf to the path of the project, with a very short timeout (5-10s)? That way the apply would error with a "timeout waiting for project to reach state" error on apply in the tf provider, and would transparently be fixed in the provider when the upstream change was made?

PatrickRice-KSC avatar Aug 05 '22 14:08 PatrickRice-KSC