func resourceGitlabProjectCreate()

in internal/provider/sdk/resource_gitlab_project.go [1020:1192]


func resourceGitlabProjectCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
	client := meta.(*gitlab.Client)

	// Project that has either been created or forked
	var project *gitlab.Project

	// The base project is created in one of two ways: We either create it from scratch,
	// or it's forked from an existing project and edited. This block checks the forked
	// status and handles the base create or forked logic
	if forkedFromProjectID, ok := d.GetOk("forked_from_project_id"); ok {
		tflog.Debug(ctx, "Creating forked project", map[string]any{
			"forked_from_project_id": forkedFromProjectID,
			"data":                   d,
		})

		createdProject, diag := createForkedProject(ctx, forkedFromProjectID.(int), d, client)
		if diag != nil {
			return diag
		}
		project = createdProject

	} else {

		tflog.Debug(ctx, "Creating project", map[string]any{
			"data": d,
		})

		createdProject, diag := createProject(ctx, d, client)
		if diag != nil {
			return diag
		}
		project = createdProject

	}

	// from this point onwards no matter how we return, resource creation
	// is committed to state since we set its ID
	d.SetId(fmt.Sprintf("%d", project.ID))

	// An import can be triggered by import_url or by creating the project from a template or from a fork.
	if project.ImportStatus != "none" {
		tflog.Debug(ctx, fmt.Sprintf("waiting for project %q import to finish", project.Name))

		stateConf := &retry.StateChangeConf{
			Pending: []string{"scheduled", "started"},
			Target:  []string{"finished"},
			Timeout: d.Timeout(schema.TimeoutCreate),
			Refresh: func() (any, string, error) {
				status, _, err := client.ProjectImportExport.ImportStatus(d.Id(), gitlab.WithContext(ctx))
				if err != nil {
					return nil, "", err
				}

				return status, status.ImportStatus, nil
			},
		}

		if _, err := stateConf.WaitForStateContext(ctx); err != nil {
			return diag.Errorf("error while waiting for project %q import to finish: %s", project.Name, err)
		}

		// Read the project again, so that we can detect the default branch.
		var err error
		project, _, err = client.Projects.GetProject(project.ID, nil, gitlab.WithContext(ctx))
		if err != nil {
			return diag.Errorf("Failed to get project %q after completing import: %s", d.Id(), err)
		}
	}

	if d.Get("archived").(bool) {
		// strange as it may seem, this project is created in archived state...
		if _, _, err := client.Projects.ArchiveProject(d.Id(), gitlab.WithContext(ctx)); err != nil {
			return diag.Errorf("new project %q could not be archived: %s", d.Id(), err)
		}
	}

	if _, ok := d.GetOk("push_rules"); ok {
		err := editOrAddPushRules(ctx, client, d.Id(), d)
		if err != nil {
			if api.Is404(err) {
				tflog.Debug(ctx, fmt.Sprintf("[DEBUG] Failed to edit push rules for project %q: %v", d.Id(), err))
				return diag.Errorf("Project push rules are not supported in your version of GitLab")
			}
			return diag.Errorf("Failed to edit push rules for project %q: %s", d.Id(), err)
		}
	}

	// If enabling Secret Push Detection, then update that value on the project via
	// GraphQL
	// nolint:staticcheck // SA1019 ignore deprecated GetOkExists
	// lintignore: XR001 // TODO: replace with alternative for GetOkExists
	if _, ok := d.GetOkExists("pre_receive_secret_detection_enabled"); ok {
		val := d.Get("pre_receive_secret_detection_enabled").(bool)
		err := updateProjectSecretDetectionValue(ctx, client, project.PathWithNamespace, val)
		if err != nil {
			return diag.Errorf("Error updating Secret Push Detection on Project %d: %v", project.ID, err)
		}
	}

	// nolint:staticcheck // SA1019 ignore deprecated GetOkExists
	// lintignore: XR001 // TODO: replace with alternative for GetOkExists
	if v, ok := d.GetOkExists("skip_wait_for_default_branch_protection"); ok {
		d.Set("skip_wait_for_default_branch_protection", v.(bool))
	}
	if !d.Get("skip_wait_for_default_branch_protection").(bool) {
		// If the project is assigned to a group namespace and the group has *default branch protection*
		// disabled (`default_branch_protection = 0`) then we don't have to wait for one.
		waitForDefaultBranchProtection, err := expectDefaultBranchProtection(ctx, client, project)
		if err != nil {
			return diag.Errorf("Failed to discover if branch protection is enabled by default or not for project %d: %+v", project.ID, err)
		}

		if waitForDefaultBranchProtection {
			var branch *gitlab.Branch

			// Branch protection for a newly created branch is an async action, so use WaitForState to ensure it's protected
			// before we continue. Note this check should only be required when there is a custom default branch set
			// See issue 800: https://gitlab.com/gitlab-org/terraform-provider-gitlab/-/issues/800
			stateConf := &retry.StateChangeConf{
				Pending: []string{"false"},
				Target:  []string{"true"},

				// The async action usually completes very quickly, within seconds. However in
				// lower compute or disk constrained environment, it can take a while.
				// When importing a project and changing the branch protection, the "TimeoutCreate" may
				// happen twice, and that's OK.
				Timeout: d.Timeout(schema.TimeoutCreate),
				Refresh: func() (any, string, error) {
					branch, _, err = client.Branches.GetBranch(project.ID, project.DefaultBranch, gitlab.WithContext(ctx))
					if err != nil {
						if api.Is404(err) {
							// When we hit a 404 here, it means the default branch wasn't created at all as part of the project
							// this will happen when "default_branch" isn't set, or "initialize_with_readme" is set to false.
							// We don't need to wait anymore, so return "true" to exist the wait loop.
							return branch, "true", nil
						}

						// This is legit error, return the error.
						tflog.Debug(ctx, "Error received when attempting to read branch protection of the default branch", map[string]any{
							"error":   err,
							"project": project,
							"branch":  project.DefaultBranch,
						})
						return nil, "", err
					}

					tflog.Debug(ctx, "Project polling for default branch status", map[string]any{
						"project":          project,
						"branch":           project.DefaultBranch,
						"protectionStatus": branch.Protected,
					})
					return branch, strconv.FormatBool(branch.Protected), nil
				},
			}

			if _, err := stateConf.WaitForStateContext(ctx); err != nil {
				return diag.Errorf("error while waiting for branch %s to reach 'protected' status; current status is protected %t, %s", branch.Name, branch.Protected, err)
			}
		}
	}

	// Create our "EditProjectOptions" call using state and the existing project
	var editProjectOptions gitlab.EditProjectOptions
	updatePostCreateEditOptions(ctx, &editProjectOptions, d, client, project)

	if (editProjectOptions != gitlab.EditProjectOptions{}) {
		if _, _, err := client.Projects.EditProject(d.Id(), &editProjectOptions, gitlab.WithContext(ctx)); err != nil {
			return diag.Errorf("Could not update project %q: %s", d.Id(), err)
		}
	}

	return resourceGitlabProjectRead(ctx, d, meta)
}