From 7ba385d44a4a8d510e534d8a8d084ddc66301971 Mon Sep 17 00:00:00 2001 From: lerentis Date: Mon, 22 Aug 2022 23:54:13 +0800 Subject: [PATCH] propose features upstream (#2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hi @techknowlogick 👋 as discussed on twitter the changes i made on my fork 😃 not sure if you are aware of this but currently hashicorp only allows publishing via github, so if you want to publish this provider to the terraform registry as well, feel free to also take a look at my goreleaser config and drone/github actions usage her: https://git.uploadfilter24.eu/lerentis/terraform-provider-gitea Co-authored-by: Tobias Trabelsi Reviewed-on: https://gitea.com/gitea/terraform-provider-gitea/pulls/2 Co-authored-by: lerentis Co-committed-by: lerentis --- Makefile | 11 + docs/data-sources/org.md | 32 ++ docs/data-sources/repo.md | 47 ++ docs/data-sources/user.md | 33 ++ docs/index.md | 52 ++ docs/resources/oauth2_app.md | 29 + docs/resources/org.md | 50 ++ docs/resources/public_key.md | 52 ++ docs/resources/repository.md | 109 ++++ docs/resources/team.md | 62 +++ docs/resources/user.md | 60 ++ examples/.gitignore | 6 + examples/provider/provider.tf | 24 + examples/resources/gitea_org/resource.tf | 8 + .../resources/gitea_public_key/id_ed25519.pub | 1 + .../resources/gitea_public_key/resource.tf | 14 + .../resources/gitea_repository/resource.tf | 26 + examples/resources/gitea_team/resource.tf | 21 + examples/resources/gitea_user/resource.tf | 7 + gitea/config.go | 5 +- gitea/provider.go | 8 +- gitea/resource_gitea_oauth_app.go | 195 +++++++ gitea/resource_gitea_organisation.go | 190 +++++++ gitea/resource_gitea_public_key.go | 155 ++++++ gitea/resource_gitea_repository.go | 523 ++++++++++++++++++ gitea/resource_gitea_team.go | 296 ++++++++++ gitea/resource_gitea_user.go | 364 ++++++++++++ 27 files changed, 2375 insertions(+), 5 deletions(-) create mode 100644 docs/data-sources/org.md create mode 100644 docs/data-sources/repo.md create mode 100644 docs/data-sources/user.md create mode 100644 docs/index.md create mode 100644 docs/resources/oauth2_app.md create mode 100644 docs/resources/org.md create mode 100644 docs/resources/public_key.md create mode 100644 docs/resources/repository.md create mode 100644 docs/resources/team.md create mode 100644 docs/resources/user.md create mode 100644 examples/.gitignore create mode 100644 examples/provider/provider.tf create mode 100644 examples/resources/gitea_org/resource.tf create mode 100644 examples/resources/gitea_public_key/id_ed25519.pub create mode 100644 examples/resources/gitea_public_key/resource.tf create mode 100644 examples/resources/gitea_repository/resource.tf create mode 100644 examples/resources/gitea_team/resource.tf create mode 100644 examples/resources/gitea_user/resource.tf create mode 100644 gitea/resource_gitea_oauth_app.go create mode 100644 gitea/resource_gitea_organisation.go create mode 100644 gitea/resource_gitea_public_key.go create mode 100644 gitea/resource_gitea_repository.go create mode 100644 gitea/resource_gitea_team.go create mode 100644 gitea/resource_gitea_user.go diff --git a/Makefile b/Makefile index 8a5c11f..6e4f53e 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,8 @@ GOFMT_FILES?=$$(find . -name '*.go' |grep -v vendor) GOFMT ?= gofmt -s +VERSION = 0.6.1 + test: fmt-check go test -i $(TEST) || exit 1 echo $(TEST) | \ @@ -28,3 +30,12 @@ fmt-check: echo "$${diff}"; \ exit 1; \ fi; +build: + go build -o terraform-provider-gitea_${VERSION} +install: build + @echo installing to + @echo ~/.terraform.d/plugins/terraform.local/local/gitea/${VERSION}/linux_amd64/terraform-provider-gitea_${VERSION} + @mkdir -p ~/.terraform.d/plugins/terraform.local/local/gitea/${VERSION}/linux_amd64 + @mv terraform-provider-gitea_${VERSION} ~/.terraform.d/plugins/terraform.local/local/gitea/${VERSION}/linux_amd64/terraform-provider-gitea_${VERSION} +doc: + tfplugindocs \ No newline at end of file diff --git a/docs/data-sources/org.md b/docs/data-sources/org.md new file mode 100644 index 0000000..2741619 --- /dev/null +++ b/docs/data-sources/org.md @@ -0,0 +1,32 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "gitea_org Data Source - terraform-provider-gitea" +subcategory: "" +description: |- + +--- + +# gitea_org (Data Source) + + + + + + +## Schema + +### Optional + +- `name` (String) + +### Read-Only + +- `avatar_url` (String) +- `description` (String) +- `full_name` (String) +- `id` (Number) The ID of this resource. +- `location` (String) +- `visibility` (String) +- `website` (String) + + diff --git a/docs/data-sources/repo.md b/docs/data-sources/repo.md new file mode 100644 index 0000000..0bbe6c6 --- /dev/null +++ b/docs/data-sources/repo.md @@ -0,0 +1,47 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "gitea_repo Data Source - terraform-provider-gitea" +subcategory: "" +description: |- + +--- + +# gitea_repo (Data Source) + + + + + + +## Schema + +### Required + +- `name` (String) +- `username` (String) + +### Read-Only + +- `clone_url` (String) +- `created` (String) +- `default_branch` (String) +- `description` (String) +- `fork` (Boolean) +- `forks` (Number) +- `full_name` (String) +- `html_url` (String) +- `id` (String) The ID of this resource. +- `mirror` (Boolean) +- `open_issue_count` (Number) +- `permission_admin` (Boolean) +- `permission_pull` (Boolean) +- `permission_push` (Boolean) +- `private` (Boolean) +- `size` (Number) +- `ssh_url` (String) +- `stars` (Number) +- `updated` (String) +- `watchers` (Number) +- `website` (String) + + diff --git a/docs/data-sources/user.md b/docs/data-sources/user.md new file mode 100644 index 0000000..b86c466 --- /dev/null +++ b/docs/data-sources/user.md @@ -0,0 +1,33 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "gitea_user Data Source - terraform-provider-gitea" +subcategory: "" +description: |- + +--- + +# gitea_user (Data Source) + + + + + + +## Schema + +### Optional + +- `username` (String) + +### Read-Only + +- `avatar_url` (String) +- `created` (String) +- `email` (String) +- `full_name` (String) +- `id` (Number) The ID of this resource. +- `is_admin` (Boolean) +- `language` (String) +- `last_login` (String) + + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..9d0f6e2 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,52 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "gitea Provider" +subcategory: "" +description: |- + +--- + +# gitea Provider + + + +## Example Usage + +```terraform +terraform { + required_providers { + gitea = { + source = "gitea/gitea" + version = "0.6.1" + } + } +} + +provider "gitea" { + base_url = var.gitea_url # optionally use GITEA_BASE_URL env var + token = var.gitea_token # optionally use GITEA_TOKEN env var + + # Username/Password authentication is mutally exclusive with token authentication + # username = var.username # optionally use GITEA_USERNAME env var + # password = var.password # optionally use GITEA_PASSWORD env var + + # A file containing the ca certificate to use in case ssl certificate is not from a standard chain + cacert_file = var.cacert_file + + # If you are running a gitea instance with self signed TLS certificates + # and you want to disable certificate validation you can deactivate it with this flag + insecure = false +} +``` + + +## Schema + +### Optional + +- `base_url` (String) The Gitea Base API URL +- `cacert_file` (String) A file containing the ca certificate to use in case ssl certificate is not from a standard chain +- `insecure` (Boolean) Disable SSL verification of API calls +- `password` (String) Password in case of using basic auth +- `token` (String) The application token used to connect to Gitea. +- `username` (String) Username in case of using basic auth diff --git a/docs/resources/oauth2_app.md b/docs/resources/oauth2_app.md new file mode 100644 index 0000000..9dd1740 --- /dev/null +++ b/docs/resources/oauth2_app.md @@ -0,0 +1,29 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "gitea_oauth2_app Resource - terraform-provider-gitea" +subcategory: "" +description: |- + Handling gitea oauth application https://docs.gitea.io/en-us/oauth2-provider/ resources +--- + +# gitea_oauth2_app (Resource) + +Handling [gitea oauth application](https://docs.gitea.io/en-us/oauth2-provider/) resources + + + + +## Schema + +### Required + +- `name` (String) OAuth Application name +- `redirect_uris` (Set of String) Accepted redirect URIs + +### Read-Only + +- `client_id` (String) OAuth2 Application client id +- `client_secret` (String, Sensitive) Oauth2 Application client secret +- `id` (String) The ID of this resource. + + diff --git a/docs/resources/org.md b/docs/resources/org.md new file mode 100644 index 0000000..d09f7c0 --- /dev/null +++ b/docs/resources/org.md @@ -0,0 +1,50 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "gitea_org Resource - terraform-provider-gitea" +subcategory: "" +description: |- + gitea_org manages a gitea organisation. + Organisations are a way to group repositories and abstract permission management in a gitea instance. +--- + +# gitea_org (Resource) + +`gitea_org` manages a gitea organisation. + +Organisations are a way to group repositories and abstract permission management in a gitea instance. + +## Example Usage + +```terraform +resource "gitea_org" "test_org" { + name = "test-org" +} + +resource "gitea_repository" "org_repo" { + username = gitea_org.test_org.name + name = "org-test-repo" +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the organisation without spaces. + +### Optional + +- `description` (String) A description of this organisation. +- `full_name` (String) The display name of the organisation. Defaults to the value of `name`. +- `location` (String) +- `repo_admin_change_team_access` (Boolean) +- `visibility` (String) Flag is this organisation should be publicly visible or not. +- `website` (String) A link to a website with more information about this organisation. + +### Read-Only + +- `avatar_url` (String) +- `id` (String) The ID of this resource. + + diff --git a/docs/resources/public_key.md b/docs/resources/public_key.md new file mode 100644 index 0000000..aba61fc --- /dev/null +++ b/docs/resources/public_key.md @@ -0,0 +1,52 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "gitea_public_key Resource - terraform-provider-gitea" +subcategory: "" +description: |- + gitea_public_key manages ssh key that are associated with users. +--- + +# gitea_public_key (Resource) + +`gitea_public_key` manages ssh key that are associated with users. + +## Example Usage + +```terraform +resource "gitea_user" "test" { + username = "test" + login_name = "test" + password = "Geheim1!" + email = "test@user.dev" + must_change_password = false +} + + +resource "gitea_public_key" "test_user_key" { + title = "test" + key = file("${path.module}/id_ed25519.pub") + username = gitea_user.test.username +} +``` + + +## Schema + +### Required + +- `key` (String, Sensitive) An armored SSH key to add +- `title` (String) Title of the key to add +- `username` (String) User to associate with the added key + +### Optional + +- `read_only` (Boolean) Describe if the key has only read access or read/write + +### Read-Only + +- `created` (String) +- `fingerprint` (String) +- `id` (String) The ID of this resource. +- `type` (String) + + diff --git a/docs/resources/repository.md b/docs/resources/repository.md new file mode 100644 index 0000000..600ba78 --- /dev/null +++ b/docs/resources/repository.md @@ -0,0 +1,109 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "gitea_repository Resource - terraform-provider-gitea" +subcategory: "" +description: |- + gitea_repository manages a gitea repository. + Per default this repository will be initializiled with the provided configuration (gitignore, License etc.). + If the username property is set to a organisation name, the provider will try to look if this organisation exists and create the repository under the organisation scope. + Repository migrations have some properties that are not available to regular repositories. These are all prefixed with migration_. +--- + +# gitea_repository (Resource) + +`gitea_repository` manages a gitea repository. + +Per default this repository will be initializiled with the provided configuration (gitignore, License etc.). +If the `username` property is set to a organisation name, the provider will try to look if this organisation exists and create the repository under the organisation scope. + +Repository migrations have some properties that are not available to regular repositories. These are all prefixed with `migration_`. + +## Example Usage + +```terraform +resource "gitea_user" "test" { + username = "test" + login_name = "test" + password = "Geheim1!" + email = "test@user.dev" + must_change_password = false +} + +resource "gitea_repository" "test" { + username = resource.gitea_user.test.name + name = "test" + private = true + issue_labels = "Default" + license = "MIT" + gitignores = "Go" +} + +resource "gitea_repository" "mirror" { + username = resource.gitea_user.test.name + name = "terraform-provider-gitea-mirror" + description = "Mirror of Terraform Provider" + mirror = true + migration_clone_addresse = "https://git.uploadfilter24.eu/lerentis/terraform-provider-gitea.git" + migration_service = "gitea" + migration_service_auth_token = var.gitea_mirror_token +} +``` + + +## Schema + +### Required + +- `name` (String) The Name of the repository +- `username` (String) The Owner of the repository + +### Optional + +- `allow_manual_merge` (Boolean) +- `allow_merge_commits` (Boolean) +- `allow_rebase` (Boolean) +- `allow_rebase_explicit` (Boolean) +- `allow_squash_merge` (Boolean) +- `archived` (Boolean) +- `auto_init` (Boolean) Flag if the repository should be initiated with the configured values +- `autodetect_manual_merge` (Boolean) +- `default_branch` (String) The default branch of the repository. Defaults to `main` +- `description` (String) The description of the repository. +- `gitignores` (String) A specific gitignore that should be commited to the repositoryon creation if `auto_init` is set to `true` +Need to exist in the gitea instance +- `has_issues` (Boolean) A flag if the repository should have issue management enabled or not. +- `has_projects` (Boolean) A flag if the repository should have the native project management enabled or not. +- `has_pull_requests` (Boolean) A flag if the repository should acceppt pull requests or not. +- `has_wiki` (Boolean) A flag if the repository should have the native wiki enabled or not. +- `ignore_whitespace_conflicts` (Boolean) +- `issue_labels` (String) The Issue Label configuration to be used in this repository. +Need to exist in the gitea instance +- `license` (String) The license under which the source code of this repository should be. +Need to exist in the gitea instance +- `migration_clone_addresse` (String) +- `migration_issue_labels` (Boolean) +- `migration_lfs` (Boolean) +- `migration_lfs_endpoint` (String) +- `migration_milestones` (Boolean) +- `migration_mirror_interval` (String) valid time units are 'h', 'm', 's'. 0 to disable automatic sync +- `migration_releases` (Boolean) +- `migration_service` (String) git/github/gitlab/gitea/gogs +- `migration_service_auth_password` (String, Sensitive) +- `migration_service_auth_token` (String, Sensitive) +- `migration_service_auth_username` (String) +- `mirror` (Boolean) +- `private` (Boolean) Flag if the repository should be private or not. +- `readme` (String) +- `repo_template` (Boolean) +- `website` (String) A link to a website with more information. + +### Read-Only + +- `created` (String) +- `id` (String) The ID of this resource. +- `permission_admin` (Boolean) +- `permission_pull` (Boolean) +- `permission_push` (Boolean) +- `updated` (String) + + diff --git a/docs/resources/team.md b/docs/resources/team.md new file mode 100644 index 0000000..6434ca7 --- /dev/null +++ b/docs/resources/team.md @@ -0,0 +1,62 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "gitea_team Resource - terraform-provider-gitea" +subcategory: "" +description: |- + gitea_team manages Team that are part of an organisation. +--- + +# gitea_team (Resource) + +`gitea_team` manages Team that are part of an organisation. + +## Example Usage + +```terraform +resource "gitea_org" "test_org" { + name = "test-org" +} + +resource "gitea_user" "test" { + username = "test" + login_name = "test" + password = "Geheim1!" + email = "test@user.dev" + must_change_password = false + admin = true +} + + +resource "gitea_team" "test_team" { + name = "Devs" + organisation = gitea_org.test_org.name + description = "Devs of Test Org" + permission = "write" + members = [gitea_user.test.username] +} +``` + + +## Schema + +### Required + +- `name` (String) Name of the Team +- `organisation` (String) The organisation which this Team is part of. + +### Optional + +- `can_create_repos` (Boolean) Flag if the Teams members should be able to create Rpositories in the Organisation +- `description` (String) Description of the Team +- `include_all_repositories` (Boolean) Flag if the Teams members should have access to all Repositories in the Organisation +- `members` (List of String) List of Users that should be part of this team +- `permission` (String) Permissions associated with this Team +Can be `none`, `read`, `write`, `admin` or `owner` +- `units` (String) List of types of Repositories that should be allowed to be created from Team members. +Can be `repo.code`, `repo.issues`, `repo.ext_issues`, `repo.wiki`, `repo.pulls`, `repo.releases`, `repo.projects` and/or `repo.ext_wiki` + +### Read-Only + +- `id` (String) The ID of this resource. + + diff --git a/docs/resources/user.md b/docs/resources/user.md new file mode 100644 index 0000000..a992c95 --- /dev/null +++ b/docs/resources/user.md @@ -0,0 +1,60 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "gitea_user Resource - terraform-provider-gitea" +subcategory: "" +description: |- + gitea_user manages a native gitea user. + If you are using OIDC or other kinds of authentication mechanisms you can still try to managessh keys or other ressources this way +--- + +# gitea_user (Resource) + +`gitea_user` manages a native gitea user. + +If you are using OIDC or other kinds of authentication mechanisms you can still try to managessh keys or other ressources this way + +## Example Usage + +```terraform +resource "gitea_user" "test" { + username = "test" + login_name = "test" + password = "Geheim1!" + email = "test@user.dev" + must_change_password = false +} +``` + + +## Schema + +### Required + +- `email` (String) E-Mail Address of the user +- `login_name` (String) The login name can differ from the username +- `password` (String, Sensitive) Password to be set for the user +- `username` (String) Username of the user to be created + +### Optional + +- `active` (Boolean) Flag if this user should be active or not +- `admin` (Boolean) Flag if this user should be an administrator or not +- `allow_create_organization` (Boolean) +- `allow_git_hook` (Boolean) +- `allow_import_local` (Boolean) +- `description` (String) A description of the user +- `force_password_change` (Boolean) Flag if the user defined password should be overwritten or not +- `full_name` (String) Full name of the user +- `location` (String) +- `max_repo_creation` (Number) +- `must_change_password` (Boolean) Flag if the user should change the password after first login +- `prohibit_login` (Boolean) Flag if the user should not be allowed to log in (bot user) +- `restricted` (Boolean) +- `send_notification` (Boolean) Flag to send a notification about the user creation to the defined `email` +- `visibility` (String) Visibility of the user. Can be `public`, `limited` or `private` + +### Read-Only + +- `id` (String) The ID of this resource. + + diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 0000000..85b87b4 --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,6 @@ +.terraform +.terraform.lock.hcl +terraform.tfstate +terraform.tfstate.backup +*.tfvars +id_ed25519 \ No newline at end of file diff --git a/examples/provider/provider.tf b/examples/provider/provider.tf new file mode 100644 index 0000000..655382c --- /dev/null +++ b/examples/provider/provider.tf @@ -0,0 +1,24 @@ +terraform { + required_providers { + gitea = { + source = "gitea/gitea" + version = "0.6.1" + } + } +} + +provider "gitea" { + base_url = var.gitea_url # optionally use GITEA_BASE_URL env var + token = var.gitea_token # optionally use GITEA_TOKEN env var + + # Username/Password authentication is mutally exclusive with token authentication + # username = var.username # optionally use GITEA_USERNAME env var + # password = var.password # optionally use GITEA_PASSWORD env var + + # A file containing the ca certificate to use in case ssl certificate is not from a standard chain + cacert_file = var.cacert_file + + # If you are running a gitea instance with self signed TLS certificates + # and you want to disable certificate validation you can deactivate it with this flag + insecure = false +} \ No newline at end of file diff --git a/examples/resources/gitea_org/resource.tf b/examples/resources/gitea_org/resource.tf new file mode 100644 index 0000000..c864c01 --- /dev/null +++ b/examples/resources/gitea_org/resource.tf @@ -0,0 +1,8 @@ +resource "gitea_org" "test_org" { + name = "test-org" +} + +resource "gitea_repository" "org_repo" { + username = gitea_org.test_org.name + name = "org-test-repo" +} \ No newline at end of file diff --git a/examples/resources/gitea_public_key/id_ed25519.pub b/examples/resources/gitea_public_key/id_ed25519.pub new file mode 100644 index 0000000..a6f4571 --- /dev/null +++ b/examples/resources/gitea_public_key/id_ed25519.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINn6hAP48oKz6MVWjYvn0fne2YeaOv/zC6zuvFXlJKf2 test@dev.local diff --git a/examples/resources/gitea_public_key/resource.tf b/examples/resources/gitea_public_key/resource.tf new file mode 100644 index 0000000..1862438 --- /dev/null +++ b/examples/resources/gitea_public_key/resource.tf @@ -0,0 +1,14 @@ +resource "gitea_user" "test" { + username = "test" + login_name = "test" + password = "Geheim1!" + email = "test@user.dev" + must_change_password = false +} + + +resource "gitea_public_key" "test_user_key" { + title = "test" + key = file("${path.module}/id_ed25519.pub") + username = gitea_user.test.username +} diff --git a/examples/resources/gitea_repository/resource.tf b/examples/resources/gitea_repository/resource.tf new file mode 100644 index 0000000..db9a55d --- /dev/null +++ b/examples/resources/gitea_repository/resource.tf @@ -0,0 +1,26 @@ +resource "gitea_user" "test" { + username = "test" + login_name = "test" + password = "Geheim1!" + email = "test@user.dev" + must_change_password = false +} + +resource "gitea_repository" "test" { + username = resource.gitea_user.test.name + name = "test" + private = true + issue_labels = "Default" + license = "MIT" + gitignores = "Go" +} + +resource "gitea_repository" "mirror" { + username = resource.gitea_user.test.name + name = "terraform-provider-gitea-mirror" + description = "Mirror of Terraform Provider" + mirror = true + migration_clone_addresse = "https://git.uploadfilter24.eu/lerentis/terraform-provider-gitea.git" + migration_service = "gitea" + migration_service_auth_token = var.gitea_mirror_token +} diff --git a/examples/resources/gitea_team/resource.tf b/examples/resources/gitea_team/resource.tf new file mode 100644 index 0000000..7accce0 --- /dev/null +++ b/examples/resources/gitea_team/resource.tf @@ -0,0 +1,21 @@ +resource "gitea_org" "test_org" { + name = "test-org" +} + +resource "gitea_user" "test" { + username = "test" + login_name = "test" + password = "Geheim1!" + email = "test@user.dev" + must_change_password = false + admin = true +} + + +resource "gitea_team" "test_team" { + name = "Devs" + organisation = gitea_org.test_org.name + description = "Devs of Test Org" + permission = "write" + members = [gitea_user.test.username] +} diff --git a/examples/resources/gitea_user/resource.tf b/examples/resources/gitea_user/resource.tf new file mode 100644 index 0000000..1b97b0f --- /dev/null +++ b/examples/resources/gitea_user/resource.tf @@ -0,0 +1,7 @@ +resource "gitea_user" "test" { + username = "test" + login_name = "test" + password = "Geheim1!" + email = "test@user.dev" + must_change_password = false +} \ No newline at end of file diff --git a/gitea/config.go b/gitea/config.go index 2003f52..39bd524 100644 --- a/gitea/config.go +++ b/gitea/config.go @@ -61,12 +61,11 @@ func (c *Config) Client() (interface{}, error) { var client *gitea.Client if c.Token != "" { - client, _ = gitea.NewClient(c.BaseURL, gitea.SetToken(c.Token)) + client, _ = gitea.NewClient(c.BaseURL, gitea.SetToken(c.Token), gitea.SetHTTPClient(httpClient)) } - client.SetHTTPClient(httpClient) if c.Username != "" { - client.SetBasicAuth(c.Username, c.Password) + client, _ = gitea.NewClient(c.BaseURL, gitea.SetBasicAuth(c.Username, c.Password), gitea.SetHTTPClient(httpClient)) } // Test the credentials by checking we can get information about the authenticated user. diff --git a/gitea/provider.go b/gitea/provider.go index 9909fae..11daf15 100644 --- a/gitea/provider.go +++ b/gitea/provider.go @@ -73,10 +73,14 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ - // "gitea_org": resourceGiteaOrg(), + "gitea_org": resourceGiteaOrg(), // "gitea_team": resourceGiteaTeam(), // "gitea_repo": resourceGiteaRepo(), - // "gitea_user": resourceGiteaUser(), + "gitea_user": resourceGiteaUser(), + "gitea_oauth2_app": resourceGiteaOauthApp(), + "gitea_repository": resourceGiteaRepository(), + "gitea_public_key": resourceGiteaPublicKey(), + "gitea_team": resourceGiteaTeam(), }, ConfigureFunc: providerConfigure, diff --git a/gitea/resource_gitea_oauth_app.go b/gitea/resource_gitea_oauth_app.go new file mode 100644 index 0000000..4ff217f --- /dev/null +++ b/gitea/resource_gitea_oauth_app.go @@ -0,0 +1,195 @@ +package gitea + +import ( + "fmt" + + "code.gitea.io/sdk/gitea" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +const ( + oauth2KeyName string = "name" + oauth2KeyRedirectURIs string = "redirect_uris" + oauth2KeyClientId string = "client_id" + oauth2KeyClientSecret string = "client_secret" +) + +func resourceGiteaOauthApp() *schema.Resource { + return &schema.Resource{ + Read: resourceOauth2AppRead, + Create: resourceOauth2AppUpcreate, + Update: resourceOauth2AppUpcreate, + Delete: resourceOauth2AppDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Schema: map[string]*schema.Schema{ + oauth2KeyName: { + Required: true, + Type: schema.TypeString, + Description: "OAuth Application name", + }, + oauth2KeyRedirectURIs: { + Required: true, + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "Accepted redirect URIs", + }, + oauth2KeyClientId: { + Type: schema.TypeString, + Computed: true, + Description: "OAuth2 Application client id", + }, + oauth2KeyClientSecret: { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + Description: "Oauth2 Application client secret", + }, + }, + Description: "Handling [gitea oauth application](https://docs.gitea.io/en-us/oauth2-provider/) resources", + } +} + +func ExpandStringList(configured []interface{}) []string { + res := make([]string, 0, len(configured)) + for _, v := range configured { + val, ok := v.(string) + if ok && val != "" { + res = append(res, (v.(string))) + } + } + return res +} + +func CollapseStringList(strlist []string) []interface{} { + res := make([]interface{}, 0, len(strlist)) + for _, v := range strlist { + res = append(res, v) + } + return res +} + +func resourceOauth2AppUpcreate(d *schema.ResourceData, meta interface{}) (err error) { + client := meta.(*gitea.Client) + + redirectURIsSchema, redirectURIsSchemaOk := d.Get(oauth2KeyRedirectURIs).(*schema.Set) + + if !redirectURIsSchemaOk { + return fmt.Errorf("attribute %s must be set to a set of strings", oauth2KeyRedirectURIs) + } + + redirectURIs := ExpandStringList(redirectURIsSchema.List()) + + name, nameOk := d.Get(oauth2KeyName).(string) + + if !nameOk { + return fmt.Errorf("attribute %s must be set and must be a string", oauth2KeyName) + } + + opts := gitea.CreateOauth2Option{ + Name: name, + RedirectURIs: redirectURIs, + } + + var oauth2 *gitea.Oauth2 + + if d.IsNewResource() { + oauth2, _, err = client.CreateOauth2(opts) + } else { + oauth2, err := searchOauth2AppByClientId(client, d.Id()) + + if err != nil { + return err + } + + oauth2, _, err = client.UpdateOauth2(oauth2.ID, opts) + } + + if err != nil { + return + } + + err = setOAuth2ResourceData(oauth2, d) + + return +} + +func searchOauth2AppByClientId(c *gitea.Client, id string) (res *gitea.Oauth2, err error) { + page := 1 + + for { + apps, _, err := c.ListOauth2(gitea.ListOauth2Option{ + ListOptions: gitea.ListOptions{ + Page: page, + PageSize: 50, + }, + }) + if err != nil { + return nil, err + } + if len(apps) == 0 { + return nil, fmt.Errorf("no oauth client can be found by id '%s'", id) + } + + for _, app := range apps { + if app.ClientID == id { + return app, nil + } + } + + page += 1 + } +} + +func resourceOauth2AppRead(d *schema.ResourceData, meta interface{}) (err error) { + client := meta.(*gitea.Client) + + app, err := searchOauth2AppByClientId(client, d.Id()) + + if err != nil { + return err + } + + err = setOAuth2ResourceData(app, d) + + return +} + +func resourceOauth2AppDelete(d *schema.ResourceData, meta interface{}) (err error) { + client := meta.(*gitea.Client) + + app, err := searchOauth2AppByClientId(client, d.Id()) + + if err != nil { + return err + } + + _, err = client.DeleteOauth2(app.ID) + + return +} + +func setOAuth2ResourceData(app *gitea.Oauth2, d *schema.ResourceData) (err error) { + d.SetId(app.ClientID) + + for k, v := range map[string]interface{}{ + oauth2KeyName: app.Name, + oauth2KeyRedirectURIs: schema.NewSet(schema.HashString, CollapseStringList(app.RedirectURIs)), + oauth2KeyClientId: app.ClientID, + } { + err = d.Set(k, v) + if err != nil { + return + } + } + + if app.ClientSecret != "" { + // Gitea API only reports client secrets if the resource is newly created + d.Set(oauth2KeyClientSecret, app.ClientSecret) + } + + return +} diff --git a/gitea/resource_gitea_organisation.go b/gitea/resource_gitea_organisation.go new file mode 100644 index 0000000..be78e41 --- /dev/null +++ b/gitea/resource_gitea_organisation.go @@ -0,0 +1,190 @@ +package gitea + +import ( + "fmt" + + "code.gitea.io/sdk/gitea" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +const ( + orgName string = "name" + orgFullName string = "full_name" + orgDescription string = "description" + orgWebsite string = "website" + orgLocation string = "location" + orgVisibility string = "visibility" + RepoAdminChangeTeamAccess string = "repo_admin_change_team_access" +) + +func resourceOrgRead(d *schema.ResourceData, meta interface{}) (err error) { + client := meta.(*gitea.Client) + + var org *gitea.Organization + var resp *gitea.Response + + org, resp, err = client.GetOrg(d.Get(orgName).(string)) + + if err != nil { + if resp.StatusCode == 404 { + d.SetId("") + return nil + } else { + return err + } + } + + err = setOrgResourceData(org, d) + + return +} + +func resourceOrgCreate(d *schema.ResourceData, meta interface{}) (err error) { + client := meta.(*gitea.Client) + + opts := gitea.CreateOrgOption{ + Name: d.Get(orgName).(string), + FullName: d.Get(orgFullName).(string), + Description: d.Get(orgDescription).(string), + Website: d.Get(orgWebsite).(string), + Location: d.Get(orgLocation).(string), + Visibility: gitea.VisibleType(d.Get(orgVisibility).(string)), + RepoAdminChangeTeamAccess: d.Get(RepoAdminChangeTeamAccess).(bool), + } + + org, _, err := client.CreateOrg(opts) + if err != nil { + return + } + + err = setOrgResourceData(org, d) + + return +} + +func resourceOrgUpdate(d *schema.ResourceData, meta interface{}) (err error) { + client := meta.(*gitea.Client) + + var org *gitea.Organization + var resp *gitea.Response + + org, resp, err = client.GetOrg(d.Get(orgName).(string)) + + if err != nil { + if resp.StatusCode == 404 { + resourceOrgCreate(d, meta) + } else { + return err + } + } + + opts := gitea.EditOrgOption{ + FullName: d.Get(orgFullName).(string), + Description: d.Get(orgDescription).(string), + Website: d.Get(orgWebsite).(string), + Location: d.Get(orgLocation).(string), + Visibility: gitea.VisibleType(d.Get(orgVisibility).(string)), + } + + client.EditOrg(d.Get(orgName).(string), opts) + + org, resp, err = client.GetOrg(d.Get(orgName).(string)) + + err = setOrgResourceData(org, d) + + return +} + +func resourceOrgDelete(d *schema.ResourceData, meta interface{}) (err error) { + client := meta.(*gitea.Client) + + var resp *gitea.Response + + resp, err = client.DeleteOrg(d.Get(orgName).(string)) + + if err != nil { + if resp.StatusCode == 404 { + return + } else { + return err + } + } + + return +} + +func setOrgResourceData(org *gitea.Organization, d *schema.ResourceData) (err error) { + d.SetId(fmt.Sprintf("%d", org.ID)) + d.Set("name", org.UserName) + d.Set("full_name", org.FullName) + d.Set("avatar_url", org.AvatarURL) + d.Set("description", org.Description) + d.Set("website", org.Website) + d.Set("location", org.Location) + d.Set("visibility", org.Visibility) + + return +} + +func resourceGiteaOrg() *schema.Resource { + return &schema.Resource{ + Read: resourceOrgRead, + Create: resourceOrgCreate, + Update: resourceOrgUpdate, + Delete: resourceOrgDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The name of the organisation without spaces.", + }, + "full_name": { + Type: schema.TypeString, + Required: false, + Optional: true, + Description: "The display name of the organisation. Defaults to the value of `name`.", + }, + "description": { + Type: schema.TypeString, + Required: false, + Optional: true, + Description: "A description of this organisation.", + }, + "website": { + Type: schema.TypeString, + Required: false, + Optional: true, + Description: "A link to a website with more information about this organisation.", + }, + "location": { + Type: schema.TypeString, + Required: false, + Optional: true, + }, + "repo_admin_change_team_access": { + Type: schema.TypeBool, + Required: false, + Optional: true, + Default: true, + }, + "avatar_url": { + Type: schema.TypeString, + Required: false, + Computed: true, + }, + "visibility": { + Type: schema.TypeString, + Required: false, + Optional: true, + Default: "public", + Description: "Flag is this organisation should be publicly visible or not.", + }, + }, + Description: "`gitea_org` manages a gitea organisation.\n\n" + + "Organisations are a way to group repositories and abstract permission management in a gitea instance.", + } +} diff --git a/gitea/resource_gitea_public_key.go b/gitea/resource_gitea_public_key.go new file mode 100644 index 0000000..c83a39f --- /dev/null +++ b/gitea/resource_gitea_public_key.go @@ -0,0 +1,155 @@ +package gitea + +import ( + "fmt" + "strconv" + + "code.gitea.io/sdk/gitea" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +const ( + PublicKeyUser string = "username" + PublicKey string = "key" + PublicKeyReadOnlyFlag string = "read_only" + PublicKeyTitle string = "title" + PublicKeyId string = "id" + PublicKeyFingerprint string = "fingerprint" + PublicKeyCreated string = "created" + PublicKeyType string = "type" +) + +func resourcePublicKeyRead(d *schema.ResourceData, meta interface{}) (err error) { + client := meta.(*gitea.Client) + + id, err := strconv.ParseInt(d.Id(), 10, 64) + + var resp *gitea.Response + var pubKey *gitea.PublicKey + + pubKey, resp, err = client.GetPublicKey(id) + + if err != nil { + if resp.StatusCode == 404 { + d.SetId("") + return nil + } else { + return err + } + } + + err = setPublicKeyResourceData(pubKey, d) + + return +} + +func resourcePublicKeyCreate(d *schema.ResourceData, meta interface{}) (err error) { + client := meta.(*gitea.Client) + + var pubKey *gitea.PublicKey + + opts := gitea.CreateKeyOption{ + Title: d.Get(PublicKeyTitle).(string), + Key: d.Get(PublicKey).(string), + ReadOnly: d.Get(PublicKeyReadOnlyFlag).(bool), + } + + pubKey, _, err = client.AdminCreateUserPublicKey(d.Get(PublicKeyUser).(string), opts) + + err = setPublicKeyResourceData(pubKey, d) + + return +} + +func resourcePublicKeyUpdate(d *schema.ResourceData, meta interface{}) (err error) { + // update = recreate + resourcePublicKeyDelete(d, meta) + resourcePublicKeyCreate(d, meta) + return +} + +func resourcePublicKeyDelete(d *schema.ResourceData, meta interface{}) (err error) { + client := meta.(*gitea.Client) + + id, err := strconv.ParseInt(d.Id(), 10, 64) + + var resp *gitea.Response + + resp, err = client.AdminDeleteUserPublicKey(d.Get(PublicKeyUser).(string), int(id)) + + if err != nil { + if resp.StatusCode == 404 { + return + } else { + return err + } + } + + return +} + +func setPublicKeyResourceData(pubKey *gitea.PublicKey, d *schema.ResourceData) (err error) { + d.SetId(fmt.Sprintf("%d", pubKey.ID)) + d.Set(PublicKeyUser, d.Get(PublicKeyUser).(string)) + d.Set(PublicKey, d.Get(PublicKey).(string)) + d.Set(PublicKeyTitle, pubKey.Title) + d.Set(PublicKeyReadOnlyFlag, d.Get(PublicKeyReadOnlyFlag).(bool)) + d.Set(PublicKeyCreated, pubKey.Created) + d.Set(PublicKeyFingerprint, pubKey.Fingerprint) + d.Set(PublicKeyType, pubKey.KeyType) + return +} + +func resourceGiteaPublicKey() *schema.Resource { + return &schema.Resource{ + Read: resourcePublicKeyRead, + Create: resourcePublicKeyCreate, + Update: resourcePublicKeyUpdate, + Delete: resourcePublicKeyDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Schema: map[string]*schema.Schema{ + "title": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Title of the key to add", + }, + "key": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Sensitive: true, + Description: "An armored SSH key to add", + }, + "read_only": { + Type: schema.TypeBool, + Required: false, + Optional: true, + Default: false, + Description: "Describe if the key has only read access or read/write", + }, + "username": { + Type: schema.TypeString, + Required: true, + Optional: false, + ForceNew: true, + Description: "User to associate with the added key", + }, + "fingerprint": { + Type: schema.TypeString, + Computed: true, + }, + "created": { + Type: schema.TypeString, + Computed: true, + }, + "type": { + Type: schema.TypeString, + Computed: true, + }, + }, + Description: "`gitea_public_key` manages ssh key that are associated with users.", + } +} diff --git a/gitea/resource_gitea_repository.go b/gitea/resource_gitea_repository.go new file mode 100644 index 0000000..5cda6ef --- /dev/null +++ b/gitea/resource_gitea_repository.go @@ -0,0 +1,523 @@ +package gitea + +import ( + "fmt" + "strconv" + + "code.gitea.io/sdk/gitea" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +const ( + repoOwner string = "username" + repoName string = "name" + repoDescription string = "description" + repoPrivateFlag string = "private" + repoIssueLabels string = "issue_labels" + repoAutoInit string = "auto_init" + repoTemplate string = "repo_template" + repoGitignores string = "gitignores" + repoLicense string = "license" + repoReadme string = "readme" + repoDefaultBranch string = "default_branch" + repoWebsite string = "website" + repoIssues string = "has_issues" + repoWiki string = "has_wiki" + repoPrs string = "has_pull_requests" + repoProjects string = "has_projects" + repoIgnoreWhitespace string = "ignore_whitespace_conflicts" + repoAllowMerge string = "allow_merge_commits" + repoAllowRebase string = "allow_rebase" + repoAllowRebaseMerge string = "allow_rebase_explicit" + repoAllowSquash string = "allow_squash_merge" + repoAchived string = "archived" + repoAllowManualMerge string = "allow_manual_merge" + repoAutodetectManualMerge string = "autodetect_manual_merge" + repoMirror string = "mirror" + migrationCloneAddress string = "migration_clone_addresse" + migrationService string = "migration_service" + migrationServiceAuthName string = "migration_service_auth_username" + migrationServiceAuthPassword string = "migration_service_auth_password" + migrationServiceAuthToken string = "migration_service_auth_token" + migrationMilestones string = "migration_milestones" + migrationReleases string = "migration_releases" + migrationIssueLabels string = "migration_issue_labels" + migrationMirrorInterval string = "migration_mirror_interval" + migrationLFS string = "migration_lfs" + migrationLFSEndpoint string = "migration_lfs_endpoint" +) + +func resourceRepoRead(d *schema.ResourceData, meta interface{}) (err error) { + client := meta.(*gitea.Client) + + id, err := strconv.ParseInt(d.Id(), 10, 64) + var resp *gitea.Response + + if err != nil { + return err + } + + repo, resp, err := client.GetRepoByID(id) + + if err != nil { + if resp.StatusCode == 404 { + d.SetId("") + return nil + } else { + return err + } + } + + err = setRepoResourceData(repo, d) + + return +} + +func resourceRepoCreate(d *schema.ResourceData, meta interface{}) (err error) { + client := meta.(*gitea.Client) + + var repo *gitea.Repository + var resp *gitea.Response + var orgRepo bool + + _, resp, err = client.GetOrg(d.Get(repoOwner).(string)) + + if resp.StatusCode == 404 { + orgRepo = false + } else { + orgRepo = true + } + + if (d.Get(repoMirror)).(bool) { + opts := gitea.MigrateRepoOption{ + RepoName: d.Get(repoName).(string), + RepoOwner: d.Get(repoOwner).(string), + CloneAddr: d.Get(migrationCloneAddress).(string), + Service: gitea.GitServiceType(d.Get(migrationService).(string)), + Mirror: d.Get(repoMirror).(bool), + Private: d.Get(repoPrivateFlag).(bool), + Description: d.Get(repoDescription).(string), + Wiki: d.Get(repoWiki).(bool), + Milestones: d.Get(migrationMilestones).(bool), + Labels: d.Get(migrationIssueLabels).(bool), + Issues: d.Get(repoIssues).(bool), + PullRequests: d.Get(repoPrs).(bool), + Releases: d.Get(migrationReleases).(bool), + MirrorInterval: d.Get(migrationMirrorInterval).(string), + LFS: d.Get(migrationLFS).(bool), + LFSEndpoint: d.Get(migrationLFSEndpoint).(string), + } + + if d.Get(migrationServiceAuthName).(string) != "" { + opts.AuthUsername = d.Get(migrationServiceAuthName).(string) + } + if d.Get(migrationServiceAuthPassword).(string) != "" { + opts.AuthPassword = d.Get(migrationServiceAuthPassword).(string) + } + if d.Get(migrationServiceAuthToken).(string) != "" { + opts.AuthToken = d.Get(migrationServiceAuthToken).(string) + } + + repo, _, err = client.MigrateRepo(opts) + + } else { + opts := gitea.CreateRepoOption{ + Name: d.Get(repoName).(string), + Description: d.Get(repoDescription).(string), + Private: d.Get(repoPrivateFlag).(bool), + IssueLabels: d.Get(repoIssueLabels).(string), + AutoInit: d.Get(repoAutoInit).(bool), + Template: d.Get(repoTemplate).(bool), + Gitignores: d.Get(repoGitignores).(string), + License: d.Get(repoLicense).(string), + Readme: d.Get(repoReadme).(string), + DefaultBranch: d.Get(repoDefaultBranch).(string), + TrustModel: "default", + } + + if orgRepo { + repo, _, err = client.CreateOrgRepo(d.Get(repoOwner).(string), opts) + } else { + repo, _, err = client.CreateRepo(opts) + } + } + + if err != nil { + return + } + + err = setRepoResourceData(repo, d) + + return +} + +func resourceRepoUpdate(d *schema.ResourceData, meta interface{}) (err error) { + client := meta.(*gitea.Client) + + var repo *gitea.Repository + + var name string = d.Get(repoName).(string) + var description string = d.Get(repoDescription).(string) + var website string = d.Get(repoWebsite).(string) + var private bool = d.Get(repoPrivateFlag).(bool) + var template bool = d.Get(repoTemplate).(bool) + var hasIssues bool = d.Get(repoIssues).(bool) + var hasWiki bool = d.Get(repoWiki).(bool) + var defaultBranch string = d.Get(repoDefaultBranch).(string) + var hasPRs bool = d.Get(repoPrs).(bool) + var hasProjects bool = d.Get(repoProjects).(bool) + var ignoreWhitespaceConflicts bool = d.Get(repoIgnoreWhitespace).(bool) + var allowMerge bool = d.Get(repoAllowMerge).(bool) + var allowRebase bool = d.Get(repoAllowRebase).(bool) + var allowRebaseMerge bool = d.Get(repoAllowRebaseMerge).(bool) + var allowSquash bool = d.Get(repoAllowSquash).(bool) + var allowManualMerge bool = d.Get(repoAllowManualMerge).(bool) + var autodetectManualMerge bool = d.Get(repoAutodetectManualMerge).(bool) + + opts := gitea.EditRepoOption{ + Name: &name, + Description: &description, + Website: &website, + Private: &private, + Template: &template, + HasIssues: &hasIssues, + HasWiki: &hasWiki, + DefaultBranch: &defaultBranch, + HasPullRequests: &hasPRs, + HasProjects: &hasProjects, + IgnoreWhitespaceConflicts: &ignoreWhitespaceConflicts, + AllowMerge: &allowMerge, + AllowRebase: &allowRebase, + AllowRebaseMerge: &allowRebaseMerge, + AllowSquash: &allowSquash, + AllowManualMerge: &allowManualMerge, + AutodetectManualMerge: &autodetectManualMerge, + } + + if d.Get(repoMirror).(bool) { + var mirrorInterval string = d.Get(migrationMirrorInterval).(string) + opts.MirrorInterval = &mirrorInterval + } else { + var archived bool = d.Get(repoAchived).(bool) + opts.Archived = &archived + } + + repo, _, err = client.EditRepo(d.Get(repoOwner).(string), d.Get(repoName).(string), opts) + + if err != nil { + return err + } + err = setRepoResourceData(repo, d) + + return + +} + +func respurceRepoDelete(d *schema.ResourceData, meta interface{}) (err error) { + client := meta.(*gitea.Client) + + client.DeleteRepo(d.Get(repoOwner).(string), d.Get(repoName).(string)) + + return +} + +func setRepoResourceData(repo *gitea.Repository, d *schema.ResourceData) (err error) { + d.SetId(fmt.Sprintf("%d", repo.ID)) + d.Set("name", repo.Name) + d.Set("description", repo.Description) + d.Set("full_name", repo.FullName) + d.Set("private", repo.Private) + d.Set("fork", repo.Fork) + d.Set("mirror", repo.Mirror) + d.Set("size", repo.Size) + d.Set("html_url", repo.HTMLURL) + d.Set("ssh_url", repo.SSHURL) + d.Set("clone_url", repo.CloneURL) + d.Set("website", repo.Website) + d.Set("stars", repo.Stars) + d.Set("forks", repo.Forks) + d.Set("watchers", repo.Watchers) + d.Set("open_issue_count", repo.OpenIssues) + d.Set("default_branch", repo.DefaultBranch) + d.Set("created", repo.Created) + d.Set("updated", repo.Updated) + d.Set("permission_admin", repo.Permissions.Admin) + d.Set("permission_push", repo.Permissions.Push) + d.Set("permission_pull", repo.Permissions.Pull) + + return +} + +func resourceGiteaRepository() *schema.Resource { + return &schema.Resource{ + Read: resourceRepoRead, + Create: resourceRepoCreate, + Update: resourceRepoUpdate, + Delete: respurceRepoDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Schema: map[string]*schema.Schema{ + "username": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The Owner of the repository", + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The Name of the repository", + }, + "auto_init": { + Type: schema.TypeBool, + Required: false, + Optional: true, + Default: true, + Description: "Flag if the repository should be initiated with the configured values", + }, + "repo_template": { + Type: schema.TypeBool, + Required: false, + Optional: true, + Default: false, + }, + "issue_labels": { + Type: schema.TypeString, + Required: false, + Optional: true, + Default: "Default", + Description: "The Issue Label configuration to be used in this repository.\n" + + "Need to exist in the gitea instance", + }, + "gitignores": { + Type: schema.TypeString, + Required: false, + Optional: true, + Default: "", + Description: "A specific gitignore that should be commited to the repository" + + "on creation if `auto_init` is set to `true`\n" + + "Need to exist in the gitea instance", + }, + "license": { + Type: schema.TypeString, + Required: false, + Optional: true, + Default: "", + Description: "The license under which the source code of this repository should be.\n" + + "Need to exist in the gitea instance", + }, + "readme": { + Type: schema.TypeString, + Required: false, + Optional: true, + Default: "", + }, + "description": { + Type: schema.TypeString, + Required: false, + Optional: true, + Default: "", + Description: "The description of the repository.", + }, + "private": { + Type: schema.TypeBool, + Required: false, + Optional: true, + Default: true, + Description: "Flag if the repository should be private or not.", + }, + "default_branch": { + Type: schema.TypeString, + Required: false, + Optional: true, + Default: "main", + Description: "The default branch of the repository. Defaults to `main`", + }, + "created": { + Type: schema.TypeString, + Computed: true, + }, + "updated": { + Type: schema.TypeString, + Computed: true, + }, + "permission_admin": { + Type: schema.TypeBool, + Computed: true, + }, + "permission_push": { + Type: schema.TypeBool, + Computed: true, + }, + "permission_pull": { + Type: schema.TypeBool, + Computed: true, + }, + "website": { + Type: schema.TypeString, + Required: false, + Optional: true, + Default: "", + Description: "A link to a website with more information.", + }, + "has_issues": { + Type: schema.TypeBool, + Required: false, + Optional: true, + Default: true, + Description: "A flag if the repository should have issue management enabled or not.", + }, + "has_wiki": { + Type: schema.TypeBool, + Required: false, + Optional: true, + Default: true, + Description: "A flag if the repository should have the native wiki enabled or not.", + }, + "has_pull_requests": { + Type: schema.TypeBool, + Required: false, + Optional: true, + Default: true, + Description: "A flag if the repository should acceppt pull requests or not.", + }, + "has_projects": { + Type: schema.TypeBool, + Required: false, + Optional: true, + Default: true, + Description: "A flag if the repository should have the native project management enabled or not.", + }, + "ignore_whitespace_conflicts": { + Type: schema.TypeBool, + Required: false, + Optional: true, + Default: true, + }, + "allow_merge_commits": { + Type: schema.TypeBool, + Required: false, + Optional: true, + Default: true, + }, + "allow_rebase": { + Type: schema.TypeBool, + Required: false, + Optional: true, + Default: true, + }, + "allow_rebase_explicit": { + Type: schema.TypeBool, + Required: false, + Optional: true, + Default: true, + }, + "allow_squash_merge": { + Type: schema.TypeBool, + Required: false, + Optional: true, + Default: true, + }, + "archived": { + Type: schema.TypeBool, + Required: false, + Optional: true, + Default: false, + }, + "allow_manual_merge": { + Type: schema.TypeBool, + Required: false, + Optional: true, + Default: true, + }, + "autodetect_manual_merge": { + Type: schema.TypeBool, + Required: false, + Optional: true, + Default: true, + }, + "mirror": { + Type: schema.TypeBool, + Required: false, + Optional: true, + Default: false, + }, + "migration_clone_addresse": { + Type: schema.TypeString, + Required: false, + Optional: true, + ForceNew: true, + }, + "migration_service": { + Type: schema.TypeString, + Required: false, + ForceNew: true, + Optional: true, + Description: "git/github/gitlab/gitea/gogs", + }, + "migration_service_auth_username": { + Type: schema.TypeString, + Required: false, + Optional: true, + Default: "", + }, + "migration_service_auth_password": { + Type: schema.TypeString, + Required: false, + Optional: true, + Sensitive: true, + Default: "", + }, + "migration_service_auth_token": { + Type: schema.TypeString, + Required: false, + Optional: true, + Sensitive: true, + Default: "", + }, + "migration_milestones": { + Type: schema.TypeBool, + Required: false, + Optional: true, + Default: true, + }, + "migration_releases": { + Type: schema.TypeBool, + Required: false, + Optional: true, + Default: true, + }, + "migration_issue_labels": { + Type: schema.TypeBool, + Required: false, + Optional: true, + Default: true, + }, + "migration_mirror_interval": { + Type: schema.TypeString, + Required: false, + Optional: true, + Default: "8h0m0s", + Description: "valid time units are 'h', 'm', 's'. 0 to disable automatic sync", + }, + "migration_lfs": { + Type: schema.TypeBool, + Required: false, + Optional: true, + }, + "migration_lfs_endpoint": { + Type: schema.TypeString, + Required: false, + Optional: true, + Default: "", + }, + }, + Description: "`gitea_repository` manages a gitea repository.\n\n" + + "Per default this repository will be initializiled with the provided configuration (gitignore, License etc.).\n" + + "If the `username` property is set to a organisation name, the provider will try to look if this organisation exists " + + "and create the repository under the organisation scope.\n\n" + + "Repository migrations have some properties that are not available to regular repositories. These are all prefixed with `migration_`.", + } +} diff --git a/gitea/resource_gitea_team.go b/gitea/resource_gitea_team.go new file mode 100644 index 0000000..8bfed82 --- /dev/null +++ b/gitea/resource_gitea_team.go @@ -0,0 +1,296 @@ +package gitea + +import ( + "fmt" + "strconv" + "strings" + + "code.gitea.io/sdk/gitea" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +const ( + TeamName string = "name" + TeamOrg string = "organisation" + TeamDescription string = "description" + TeamPermissions string = "permission" + TeamCreateRepoFlag string = "can_create_repos" + TeamIncludeAllReposFlag string = "include_all_repositories" + TeamUnits string = "units" + TeamMembers string = "members" +) + +func resourceTeamRead(d *schema.ResourceData, meta interface{}) (err error) { + client := meta.(*gitea.Client) + + id, err := strconv.ParseInt(d.Id(), 10, 64) + + var resp *gitea.Response + var team *gitea.Team + + team, resp, err = client.GetTeam(id) + + if err != nil { + if resp.StatusCode == 404 { + d.SetId("") + return nil + } else { + return err + } + } + + err = setTeamResourceData(team, d) + + return +} + +func resourceTeamCreate(d *schema.ResourceData, meta interface{}) (err error) { + client := meta.(*gitea.Client) + + var team *gitea.Team + var units []gitea.RepoUnitType + + if strings.Contains(d.Get(TeamUnits).(string), "repo.code") { + units = append(units, gitea.RepoUnitCode) + } + if strings.Contains(d.Get(TeamUnits).(string), "repo.issues") { + units = append(units, gitea.RepoUnitIssues) + } + if strings.Contains(d.Get(TeamUnits).(string), "repo.ext_issues") { + units = append(units, gitea.RepoUnitExtIssues) + } + if strings.Contains(d.Get(TeamUnits).(string), "repo.wiki") { + units = append(units, gitea.RepoUnitWiki) + } + if strings.Contains(d.Get(TeamUnits).(string), "repo.pulls") { + units = append(units, gitea.RepoUnitPulls) + } + if strings.Contains(d.Get(TeamUnits).(string), "repo.releases") { + units = append(units, gitea.RepoUnitReleases) + } + if strings.Contains(d.Get(TeamUnits).(string), "repo.ext_wiki") { + units = append(units, gitea.RepoUnitExtWiki) + } + if strings.Contains(d.Get(TeamUnits).(string), "repo.projects") { + units = append(units, gitea.RepoUnitProjects) + } + + opts := gitea.CreateTeamOption{ + Name: d.Get(TeamName).(string), + Description: d.Get(TeamDescription).(string), + Permission: gitea.AccessMode(d.Get(TeamPermissions).(string)), + CanCreateOrgRepo: d.Get(TeamCreateRepoFlag).(bool), + IncludesAllRepositories: d.Get(TeamIncludeAllReposFlag).(bool), + Units: units, + } + + team, _, err = client.CreateTeam(d.Get(TeamOrg).(string), opts) + + if err != nil { + return + } + + users := d.Get(TeamMembers).([]interface{}) + + for _, user := range users { + if user != "" { + _, err = client.AddTeamMember(team.ID, user.(string)) + if err != nil { + return err + } + } + } + + err = setTeamResourceData(team, d) + + return +} + +func resourceTeamUpdate(d *schema.ResourceData, meta interface{}) (err error) { + client := meta.(*gitea.Client) + + id, err := strconv.ParseInt(d.Id(), 10, 64) + + var resp *gitea.Response + var team *gitea.Team + + team, resp, err = client.GetTeam(id) + + if err != nil { + if resp.StatusCode == 404 { + resourceTeamCreate(d, meta) + } else { + return err + } + } + + description := d.Get(TeamDescription).(string) + canCreateRepo := d.Get(TeamCreateRepoFlag).(bool) + includeAllRepos := d.Get(TeamIncludeAllReposFlag).(bool) + + var units []gitea.RepoUnitType + + if strings.Contains(d.Get(TeamUnits).(string), "repo.code") { + units = append(units, gitea.RepoUnitCode) + } + if strings.Contains(d.Get(TeamUnits).(string), "repo.issues") { + units = append(units, gitea.RepoUnitIssues) + } + if strings.Contains(d.Get(TeamUnits).(string), "repo.ext_issues") { + units = append(units, gitea.RepoUnitExtIssues) + } + if strings.Contains(d.Get(TeamUnits).(string), "repo.wiki") { + units = append(units, gitea.RepoUnitWiki) + } + if strings.Contains(d.Get(TeamUnits).(string), "repo.pulls") { + units = append(units, gitea.RepoUnitPulls) + } + if strings.Contains(d.Get(TeamUnits).(string), "repo.releases") { + units = append(units, gitea.RepoUnitReleases) + } + if strings.Contains(d.Get(TeamUnits).(string), "repo.ext_wiki") { + units = append(units, gitea.RepoUnitExtWiki) + } + if strings.Contains(d.Get(TeamUnits).(string), "repo.projects") { + units = append(units, gitea.RepoUnitProjects) + } + + opts := gitea.EditTeamOption{ + Name: d.Get(TeamName).(string), + Description: &description, + Permission: gitea.AccessMode(d.Get(TeamPermissions).(string)), + CanCreateOrgRepo: &canCreateRepo, + IncludesAllRepositories: &includeAllRepos, + Units: units, + } + + resp, err = client.EditTeam(id, opts) + + if err != nil { + return err + } + + users := d.Get(TeamMembers).([]interface{}) + + for _, user := range users { + if user != "" { + _, err = client.AddTeamMember(team.ID, user.(string)) + if err != nil { + return err + } + } + } + + team, _, _ = client.GetTeam(id) + + err = setTeamResourceData(team, d) + + return +} + +func resourceTeamDelete(d *schema.ResourceData, meta interface{}) (err error) { + client := meta.(*gitea.Client) + + id, err := strconv.ParseInt(d.Id(), 10, 64) + + var resp *gitea.Response + + resp, err = client.DeleteTeam(id) + + if err != nil { + if resp.StatusCode == 404 { + return + } else { + return err + } + } + + return +} + +func setTeamResourceData(team *gitea.Team, d *schema.ResourceData) (err error) { + d.SetId(fmt.Sprintf("%d", team.ID)) + d.Set(TeamCreateRepoFlag, team.CanCreateOrgRepo) + d.Set(TeamDescription, team.Description) + d.Set(TeamName, team.Name) + d.Set(TeamPermissions, string(team.Permission)) + d.Set(TeamIncludeAllReposFlag, team.IncludesAllRepositories) + d.Set(TeamUnits, d.Get(TeamUnits).(string)) + d.Set(TeamOrg, d.Get(TeamOrg).(string)) + d.Set(TeamMembers, d.Get(TeamMembers)) + return +} + +func resourceGiteaTeam() *schema.Resource { + return &schema.Resource{ + Read: resourceTeamRead, + Create: resourceTeamCreate, + Update: resourceTeamUpdate, + Delete: resourceTeamDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Name of the Team", + }, + "organisation": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The organisation which this Team is part of.", + }, + "description": { + Type: schema.TypeString, + Required: false, + Optional: true, + Default: "", + Description: "Description of the Team", + }, + "permission": { + Type: schema.TypeString, + Required: false, + Optional: true, + Default: "", + Description: "Permissions associated with this Team\n" + + "Can be `none`, `read`, `write`, `admin` or `owner`", + }, + "can_create_repos": { + Type: schema.TypeBool, + Required: false, + Optional: true, + Default: true, + Description: "Flag if the Teams members should be able to create Rpositories in the Organisation", + }, + "include_all_repositories": { + Type: schema.TypeBool, + Required: false, + Optional: true, + Default: true, + Description: "Flag if the Teams members should have access to all Repositories in the Organisation", + }, + "units": { + Type: schema.TypeString, + Required: false, + Optional: true, + Default: "[repo.code, repo.issues, repo.ext_issues, repo.wiki, repo.pulls, repo.releases, repo.projects, repo.ext_wiki]", + Description: "List of types of Repositories that should be allowed to be created from Team members.\n" + + "Can be `repo.code`, `repo.issues`, `repo.ext_issues`, `repo.wiki`, `repo.pulls`, `repo.releases`, `repo.projects` and/or `repo.ext_wiki`", + }, + "members": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + Required: false, + Computed: true, + Description: "List of Users that should be part of this team", + }, + }, + Description: "`gitea_team` manages Team that are part of an organisation.", + } +} diff --git a/gitea/resource_gitea_user.go b/gitea/resource_gitea_user.go new file mode 100644 index 0000000..432b24e --- /dev/null +++ b/gitea/resource_gitea_user.go @@ -0,0 +1,364 @@ +package gitea + +import ( + "fmt" + "strconv" + + "code.gitea.io/sdk/gitea" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +const ( + userName string = "username" + userLoginName string = "login_name" + userEmail string = "email" + userFullName string = "full_name" + userPassword string = "password" + userMustChangePassword string = "must_change_password" + userSendNotification string = "send_notification" + userVisibility string = "visibility" + userDescription string = "description" + userLocation string = "location" + userActive string = "active" + userAdmin string = "admin" + userAllowGitHook string = "allow_git_hook" + userAllowLocalImport string = "allow_import_local" + userMaxRepoCreation string = "max_repo_creation" + userPhorbitLogin string = "prohibit_login" + userAllowCreateOrgs string = "allow_create_organization" + userRestricted string = "restricted" + userForcePasswordChange string = "force_password_change" +) + +func resourceUserRead(d *schema.ResourceData, meta interface{}) (err error) { + client := meta.(*gitea.Client) + + id, err := strconv.ParseInt(d.Id(), 10, 64) + + var resp *gitea.Response + var user *gitea.User + + user, resp, err = client.GetUserByID(id) + + if err != nil { + if resp.StatusCode == 404 { + d.SetId("") + return nil + } else { + return err + } + } + + err = setUserResourceData(user, d) + + return +} + +func resourceUserCreate(d *schema.ResourceData, meta interface{}) (err error) { + client := meta.(*gitea.Client) + + var user *gitea.User + visibility := gitea.VisibleType(d.Get(userVisibility).(string)) + changePassword := d.Get(userMustChangePassword).(bool) + + opts := gitea.CreateUserOption{ + SourceID: 0, + LoginName: d.Get(userLoginName).(string), + Username: d.Get(userName).(string), + FullName: d.Get(userFullName).(string), + Email: d.Get(userEmail).(string), + Password: d.Get(userPassword).(string), + MustChangePassword: &changePassword, + SendNotify: d.Get(userSendNotification).(bool), + Visibility: &visibility, + } + + user, _, err = client.AdminCreateUser(opts) + if err != nil { + return + } + + d.SetId(fmt.Sprintf("%d", user.ID)) + + err = resourceUserUpdate(d, meta) + + return +} + +func resourceUserUpdate(d *schema.ResourceData, meta interface{}) (err error) { + client := meta.(*gitea.Client) + + id, err := strconv.ParseInt(d.Id(), 10, 64) + var resp *gitea.Response + var user *gitea.User + + user, resp, err = client.GetUserByID(id) + + if err != nil { + if resp.StatusCode == 404 { + resourceUserCreate(d, meta) + } else { + return err + } + } + + mail := d.Get(userEmail).(string) + fullName := d.Get(userFullName).(string) + description := d.Get(userDescription).(string) + changePassword := d.Get(userMustChangePassword).(bool) + location := d.Get(userLocation).(string) + active := d.Get(userActive).(bool) + admin := d.Get(userAdmin).(bool) + allowHook := d.Get(userAllowGitHook).(bool) + allowImport := d.Get(userAllowLocalImport).(bool) + maxRepoCreation := d.Get(userMaxRepoCreation).(int) + accessDenied := d.Get(userPhorbitLogin).(bool) + allowOrgs := d.Get(userAllowCreateOrgs).(bool) + restricted := d.Get(userRestricted).(bool) + visibility := gitea.VisibleType(d.Get(userVisibility).(string)) + + if d.Get(userForcePasswordChange).(bool) { + opts := gitea.EditUserOption{ + SourceID: 0, + LoginName: d.Get(userLoginName).(string), + Email: &mail, + FullName: &fullName, + Password: d.Get(userPassword).(string), + Description: &description, + MustChangePassword: &changePassword, + Location: &location, + Active: &active, + Admin: &admin, + AllowGitHook: &allowHook, + AllowImportLocal: &allowImport, + MaxRepoCreation: &maxRepoCreation, + ProhibitLogin: &accessDenied, + AllowCreateOrganization: &allowOrgs, + Restricted: &restricted, + Visibility: &visibility, + } + _, err = client.AdminEditUser(d.Get(userName).(string), opts) + + if err != nil { + return err + } + + } else { + opts := gitea.EditUserOption{ + SourceID: 0, + LoginName: d.Get(userLoginName).(string), + Email: &mail, + FullName: &fullName, + Description: &description, + MustChangePassword: &changePassword, + Location: &location, + Active: &active, + Admin: &admin, + AllowGitHook: &allowHook, + AllowImportLocal: &allowImport, + MaxRepoCreation: &maxRepoCreation, + ProhibitLogin: &accessDenied, + AllowCreateOrganization: &allowOrgs, + Restricted: &restricted, + Visibility: &visibility, + } + _, err = client.AdminEditUser(d.Get(userName).(string), opts) + + if err != nil { + return err + } + } + + user, _, err = client.GetUserByID(id) + + err = setUserResourceData(user, d) + + return +} + +func resourceUserDelete(d *schema.ResourceData, meta interface{}) (err error) { + client := meta.(*gitea.Client) + + var resp *gitea.Response + + resp, err = client.AdminDeleteUser(d.Get(userName).(string)) + + if err != nil { + if resp.StatusCode == 404 { + return + } else { + return err + } + } + + return +} + +func setUserResourceData(user *gitea.User, d *schema.ResourceData) (err error) { + d.SetId(fmt.Sprintf("%d", user.ID)) + d.Set(userName, user.UserName) + d.Set(userEmail, user.Email) + d.Set(userFullName, user.FullName) + d.Set(userAdmin, user.IsAdmin) + d.Set("created", user.Created) + d.Set("avatar_url", user.AvatarURL) + d.Set("last_login", user.LastLogin) + d.Set("language", user.Language) + d.Set(userLoginName, d.Get(userLoginName).(string)) + d.Set(userMustChangePassword, d.Get(userMustChangePassword).(bool)) + d.Set(userSendNotification, d.Get(userSendNotification).(bool)) + d.Set(userVisibility, d.Get(userVisibility).(string)) + d.Set(userDescription, d.Get(userDescription).(string)) + d.Set(userLocation, d.Get(userLocation).(string)) + d.Set(userActive, d.Get(userActive).(bool)) + d.Set(userAllowGitHook, d.Get(userAllowGitHook).(bool)) + d.Set(userAllowLocalImport, d.Get(userAllowLocalImport).(bool)) + d.Set(userMaxRepoCreation, d.Get(userMaxRepoCreation).(int)) + d.Set(userPhorbitLogin, d.Get(userPhorbitLogin).(bool)) + d.Set(userAllowCreateOrgs, d.Get(userAllowCreateOrgs).(bool)) + d.Set(userRestricted, d.Get(userRestricted).(bool)) + d.Set(userForcePasswordChange, d.Get(userForcePasswordChange).(bool)) + + return +} + +func resourceGiteaUser() *schema.Resource { + return &schema.Resource{ + Read: resourceUserRead, + Create: resourceUserCreate, + Update: resourceUserUpdate, + Delete: resourceUserDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Schema: map[string]*schema.Schema{ + "username": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Username of the user to be created", + }, + "login_name": { + Type: schema.TypeString, + Optional: false, + Required: true, + Description: "The login name can differ from the username", + }, + "email": { + Type: schema.TypeString, + Optional: false, + Required: true, + Description: "E-Mail Address of the user", + }, + "full_name": { + Type: schema.TypeString, + Computed: true, + Optional: true, + Required: false, + Description: "Full name of the user", + }, + "password": { + Type: schema.TypeString, + Optional: false, + Required: true, + Sensitive: true, + Description: "Password to be set for the user", + }, + "must_change_password": { + Type: schema.TypeBool, + Optional: true, + Required: false, + Default: true, + Description: "Flag if the user should change the password after first login", + }, + "send_notification": { + Type: schema.TypeBool, + Optional: true, + Required: false, + Default: true, + Description: "Flag to send a notification about the user creation to the defined `email`", + }, + "visibility": { + Type: schema.TypeString, + Optional: true, + Required: false, + Default: "public", + Description: "Visibility of the user. Can be `public`, `limited` or `private`", + }, + "description": { + Type: schema.TypeString, + Optional: true, + Required: false, + Default: "", + Description: "A description of the user", + }, + "location": { + Type: schema.TypeString, + Optional: true, + Required: false, + Default: "", + }, + "active": { + Type: schema.TypeBool, + Optional: true, + Required: false, + Default: true, + Description: "Flag if this user should be active or not", + }, + "admin": { + Type: schema.TypeBool, + Optional: true, + Required: false, + Default: false, + Description: "Flag if this user should be an administrator or not", + }, + "allow_git_hook": { + Type: schema.TypeBool, + Optional: true, + Required: false, + Default: true, + }, + "allow_import_local": { + Type: schema.TypeBool, + Optional: true, + Required: false, + Default: true, + }, + "max_repo_creation": { + Type: schema.TypeInt, + Optional: true, + Required: false, + Default: -1, + }, + "prohibit_login": { + Type: schema.TypeBool, + Optional: true, + Required: false, + Default: false, + Description: "Flag if the user should not be allowed to log in (bot user)", + }, + "allow_create_organization": { + Type: schema.TypeBool, + Optional: true, + Required: false, + Default: true, + }, + "restricted": { + Type: schema.TypeBool, + Optional: true, + Required: false, + Default: false, + }, + "force_password_change": { + Type: schema.TypeBool, + Optional: true, + Required: false, + Default: false, + Description: "Flag if the user defined password should be overwritten or not", + }, + }, + Description: "`gitea_user` manages a native gitea user.\n\n" + + "If you are using OIDC or other kinds of authentication mechanisms you can still try to manage" + + "ssh keys or other ressources this way", + } +}