diff --git a/docs/data-sources/organization_private_registry.md b/docs/data-sources/organization_private_registry.md new file mode 100644 index 0000000000..50ca688e10 --- /dev/null +++ b/docs/data-sources/organization_private_registry.md @@ -0,0 +1,48 @@ +--- +page_title: "github_organization_private_registry (Data Source) - GitHub" +subcategory: "" +description: |- + Use this data source to retrieve information about a specific organization private registry. +--- + +# github_organization_private_registry (Data Source) + +Use this data source to retrieve information about a specific organization private registry. + +## Example Usage + +```terraform +data "github_organization_private_registry" "my_registry" { + name = "NPM_REGISTRY_SECRET_1" +} +``` + + +## Schema + +### Required + +- `name` (String) The auto-generated name of the private registry (computed by GitHub). + +### Read-Only + +- `auth_type` (String) The authentication type for the private registry. +- `created_at` (String) The time the registry was created. +- `id` (String) The ID of this resource. +- `oidc_audience` (String) The JWT audience. +- `oidc_aws_account_id` (String) The AWS account ID. +- `oidc_aws_domain` (String) The AWS domain. +- `oidc_aws_domain_owner` (String) The AWS domain owner. +- `oidc_aws_region` (String) The AWS region. +- `oidc_aws_role_name` (String) The AWS role name. +- `oidc_azure_client_id` (String) The Azure client ID. +- `oidc_azure_tenant_id` (String) The Azure tenant ID. +- `oidc_jfrog_identity_mapping_name` (String) The JFrog identity mapping name. +- `oidc_jfrog_provider_name` (String) The JFrog provider name. +- `registry_type` (String) The registry type. +- `replaces_base` (Boolean) Whether the private registry should replace the public base registry. +- `selected_repository_ids` (Set of Number) An array of repository IDs that can access the organization private registry. +- `updated_at` (String) The time the registry was updated. +- `url` (String) The registry URL. +- `username` (String) The registry username. +- `visibility` (String) Configures the access that repositories have to the organization private registry. diff --git a/docs/resources/organization_private_registry.md b/docs/resources/organization_private_registry.md new file mode 100644 index 0000000000..3143d14d54 --- /dev/null +++ b/docs/resources/organization_private_registry.md @@ -0,0 +1,59 @@ +--- +page_title: "github_organization_private_registry (Resource) - GitHub" +subcategory: "" +description: |- + This resource allows you to create and manage an organization private registry. +--- + +# github_organization_private_registry (Resource) + +This resource allows you to create and manage an organization private registry. + +## Example Usage + +```terraform +resource "github_organization_private_registry" "my_registry" { + registry_type = "npm_registry" + url = "https://npm.pkg.github.com" + auth_type = "username_password" + username = "github-actions" + value = "super_secret_token_123" + visibility = "private" +} +``` + + +## Schema + +### Required + +- `registry_type` (String) The registry type. Can be `maven_repository`, `nuget_feed`, `goproxy_server`, `npm_registry`, `rubygems_server`, `cargo_registry`, `composer_repository`, `docker_registry`, `git_source`, `helm_registry`, `pub_repository`, `python_index`, or `terraform_registry`. +- `url` (String) The URL of the private registry. +- `visibility` (String) Configures the access that repositories have to the organization private registry. Must be one of `all`, `private`, or `selected`. + +### Optional + +- `auth_type` (String) The authentication type for the private registry. Can be `token`, `username_password`, `oidc_azure`, `oidc_aws`, or `oidc_jfrog`. Defaults to `token`. +- `key_id` (String) ID of the public key used to encrypt the secret. Required if encrypted_value is set. +- `oidc_audience` (String) The OIDC audience. +- `oidc_aws_account_id` (String) The AWS account ID. Required when auth_type is oidc_aws. +- `oidc_aws_domain` (String) The CodeArtifact domain. Required when auth_type is oidc_aws. +- `oidc_aws_domain_owner` (String) The CodeArtifact domain owner. Required when auth_type is oidc_aws. +- `oidc_aws_region` (String) The AWS region. Required when auth_type is oidc_aws. +- `oidc_aws_role_name` (String) The AWS IAM role name. Required when auth_type is oidc_aws. +- `oidc_azure_client_id` (String) The client ID of the Azure AD application. Required when auth_type is oidc_azure. +- `oidc_azure_tenant_id` (String) The tenant ID of the Azure AD application. Required when auth_type is oidc_azure. +- `oidc_jfrog_identity_mapping_name` (String) The JFrog identity mapping name. +- `oidc_jfrog_provider_name` (String) The JFrog OIDC provider name. Required when auth_type is oidc_jfrog. +- `replaces_base` (Boolean) Indicates whether this private registry should replace the base registry. +- `selected_repository_ids` (Set of Number) An array of repository IDs that can access the organization private registry. +- `username` (String) The username to use when authenticating with the private registry. +- `value` (String, Sensitive) The plaintext secret to be encrypted and sent to GitHub. This is used for a token when auth_type is token, and for a password when auth_type is username_password. Required when auth_type is token or username_password. +- `value_encrypted` (String, Sensitive) The encrypted value of the secret using the GitHub public key in Base64 format. + +### Read-Only + +- `created_at` (String) The timestamp when the private registry was created. +- `id` (String) The ID of this resource. +- `name` (String) The auto-generated name of the private registry (computed by GitHub). +- `updated_at` (String) The timestamp when the private registry was last updated. diff --git a/examples/data-sources/github_organization_private_registry/data-source.tf b/examples/data-sources/github_organization_private_registry/data-source.tf new file mode 100644 index 0000000000..3f1e7d7943 --- /dev/null +++ b/examples/data-sources/github_organization_private_registry/data-source.tf @@ -0,0 +1,3 @@ +data "github_organization_private_registry" "my_registry" { + name = "NPM_REGISTRY_SECRET_1" +} diff --git a/examples/resources/github_organization_private_registry/resource.tf b/examples/resources/github_organization_private_registry/resource.tf new file mode 100644 index 0000000000..d91dc936f5 --- /dev/null +++ b/examples/resources/github_organization_private_registry/resource.tf @@ -0,0 +1,8 @@ +resource "github_organization_private_registry" "my_registry" { + registry_type = "npm_registry" + url = "https://npm.pkg.github.com" + auth_type = "username_password" + username = "github-actions" + value = "super_secret_token_123" + visibility = "private" +} diff --git a/github/data_source_github_organization_private_registry.go b/github/data_source_github_organization_private_registry.go new file mode 100644 index 0000000000..c623d4fd9d --- /dev/null +++ b/github/data_source_github_organization_private_registry.go @@ -0,0 +1,205 @@ +package github + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceGithubOrganizationPrivateRegistry() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceGithubOrganizationPrivateRegistryRead, + Description: "Use this data source to retrieve information about a specific organization private registry.", + Schema: map[string]*schema.Schema{ + "name": { + Description: "The auto-generated name of the private registry (computed by GitHub).", + Type: schema.TypeString, + Required: true, + }, + "registry_type": { + Description: "The registry type.", + Type: schema.TypeString, + Computed: true, + }, + "url": { + Description: "The registry URL.", + Type: schema.TypeString, + Computed: true, + }, + "username": { + Description: "The registry username.", + Type: schema.TypeString, + Computed: true, + }, + "replaces_base": { + Description: "Whether the private registry should replace the public base registry.", + Type: schema.TypeBool, + Computed: true, + }, + "visibility": { + Description: "Configures the access that repositories have to the organization private registry.", + Type: schema.TypeString, + Computed: true, + }, + "auth_type": { + Description: "The authentication type for the private registry.", + Type: schema.TypeString, + Computed: true, + }, + "oidc_azure_tenant_id": { + Description: "The Azure tenant ID.", + Type: schema.TypeString, + Computed: true, + }, + "oidc_azure_client_id": { + Description: "The Azure client ID.", + Type: schema.TypeString, + Computed: true, + }, + "oidc_aws_region": { + Description: "The AWS region.", + Type: schema.TypeString, + Computed: true, + }, + "oidc_aws_account_id": { + Description: "The AWS account ID.", + Type: schema.TypeString, + Computed: true, + }, + "oidc_aws_role_name": { + Description: "The AWS role name.", + Type: schema.TypeString, + Computed: true, + }, + "oidc_aws_domain": { + Description: "The AWS domain.", + Type: schema.TypeString, + Computed: true, + }, + "oidc_aws_domain_owner": { + Description: "The AWS domain owner.", + Type: schema.TypeString, + Computed: true, + }, + "oidc_jfrog_provider_name": { + Description: "The JFrog provider name.", + Type: schema.TypeString, + Computed: true, + }, + "oidc_audience": { + Description: "The JWT audience.", + Type: schema.TypeString, + Computed: true, + }, + "oidc_jfrog_identity_mapping_name": { + Description: "The JFrog identity mapping name.", + Type: schema.TypeString, + Computed: true, + }, + "selected_repository_ids": { + Description: "An array of repository IDs that can access the organization private registry.", + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeInt, + }, + }, + "created_at": { + Description: "The time the registry was created.", + Type: schema.TypeString, + Computed: true, + }, + "updated_at": { + Description: "The time the registry was updated.", + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceGithubOrganizationPrivateRegistryRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + org := meta.(*Owner).name + registryName := d.Get("name").(string) + + registry, _, err := client.PrivateRegistries.GetOrganizationPrivateRegistry(ctx, org, registryName) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(registry.GetName()) + + if registry.RegistryType != nil { + if err := d.Set("registry_type", string(*registry.RegistryType)); err != nil { + return diag.FromErr(err) + } + } + if err := d.Set("url", registry.GetURL()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("username", registry.GetUsername()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("replaces_base", registry.GetReplacesBase()); err != nil { + return diag.FromErr(err) + } + if registry.Visibility != nil { + if err := d.Set("visibility", string(*registry.Visibility)); err != nil { + return diag.FromErr(err) + } + } + + if registry.AuthType != nil { + if err := d.Set("auth_type", string(*registry.AuthType)); err != nil { + return diag.FromErr(err) + } + } + if err := d.Set("oidc_azure_tenant_id", registry.GetTenantID()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("oidc_azure_client_id", registry.GetClientID()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("oidc_aws_region", registry.GetAWSRegion()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("oidc_aws_account_id", registry.GetAccountID()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("oidc_aws_role_name", registry.GetRoleName()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("oidc_aws_domain", registry.GetDomain()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("oidc_aws_domain_owner", registry.GetDomainOwner()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("oidc_jfrog_provider_name", registry.GetJFrogOIDCProviderName()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("oidc_audience", registry.GetAudience()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("oidc_jfrog_identity_mapping_name", registry.GetIdentityMappingName()); err != nil { + return diag.FromErr(err) + } + + var repoIDs []any + for _, id := range registry.SelectedRepositoryIDs { + repoIDs = append(repoIDs, int(id)) + } + if err := d.Set("selected_repository_ids", schema.NewSet(schema.HashInt, repoIDs)); err != nil { + return diag.FromErr(err) + } + if err := d.Set("created_at", registry.GetCreatedAt().String()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("updated_at", registry.GetUpdatedAt().String()); err != nil { + return diag.FromErr(err) + } + + return nil +} diff --git a/github/data_source_github_organization_private_registry_test.go b/github/data_source_github_organization_private_registry_test.go new file mode 100644 index 0000000000..619b4e5769 --- /dev/null +++ b/github/data_source_github_organization_private_registry_test.go @@ -0,0 +1,46 @@ +package github + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestAccGithubDataSourceOrganizationPrivateRegistry(t *testing.T) { + config := ` + resource "github_organization_private_registry" "test" { + registry_type = "npm_registry" + url = "https://npm.pkg.github.com" + username = "github-actions" + value = "super_secret_token_123" + visibility = "private" + } + + data "github_organization_private_registry" "test" { + name = github_organization_private_registry.test.name + } + ` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + skipUnlessMode(t, organization) + skipUnlessHasOrgs(t) + }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("data.github_organization_private_registry.test", tfjsonpath.New("id"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("data.github_organization_private_registry.test", tfjsonpath.New("registry_type"), knownvalue.StringExact("npm_registry")), + statecheck.ExpectKnownValue("data.github_organization_private_registry.test", tfjsonpath.New("url"), knownvalue.StringExact("https://npm.pkg.github.com")), + statecheck.ExpectKnownValue("data.github_organization_private_registry.test", tfjsonpath.New("username"), knownvalue.StringExact("github-actions")), + statecheck.ExpectKnownValue("data.github_organization_private_registry.test", tfjsonpath.New("visibility"), knownvalue.StringExact("private")), + }, + }, + }, + }) +} diff --git a/github/provider.go b/github/provider.go index 73e93dc5e5..90d7b52f66 100644 --- a/github/provider.go +++ b/github/provider.go @@ -212,6 +212,7 @@ func NewProvider(version, commit string) func() *schema.Provider { "github_organization_block": resourceOrganizationBlock(), "github_organization_custom_role": resourceGithubOrganizationCustomRole(), "github_organization_custom_properties": resourceGithubOrganizationCustomProperties(), + "github_organization_private_registry": resourceGithubOrganizationPrivateRegistry(), "github_organization_project": resourceGithubOrganizationProject(), "github_organization_repository_role": resourceGithubOrganizationRepositoryRole(), "github_organization_role": resourceGithubOrganizationRole(), @@ -300,6 +301,7 @@ func NewProvider(version, commit string) func() *schema.Provider { "github_organization_custom_properties": dataSourceGithubOrganizationCustomProperties(), "github_organization_external_identities": dataSourceGithubOrganizationExternalIdentities(), "github_organization_ip_allow_list": dataSourceGithubOrganizationIpAllowList(), + "github_organization_private_registry": dataSourceGithubOrganizationPrivateRegistry(), "github_organization_members": dataSourceGithubOrganizationMembers(), "github_organization_repositories": dataSourceGithubOrganizationRepositories(), "github_organization_repository_role": dataSourceGithubOrganizationRepositoryRole(), diff --git a/github/resource_github_organization_private_registry.go b/github/resource_github_organization_private_registry.go new file mode 100644 index 0000000000..4efa293ece --- /dev/null +++ b/github/resource_github_organization_private_registry.go @@ -0,0 +1,485 @@ +package github + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + + "github.com/google/go-github/v89/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceGithubOrganizationPrivateRegistry() *schema.Resource { + return &schema.Resource{ + Description: "This resource allows you to create and manage an organization private registry.", + CreateContext: resourceGithubOrganizationPrivateRegistryCreate, + ReadContext: resourceGithubOrganizationPrivateRegistryRead, + UpdateContext: resourceGithubOrganizationPrivateRegistryUpdate, + DeleteContext: resourceGithubOrganizationPrivateRegistryDelete, + CustomizeDiff: customdiff.All(resourceGithubOrganizationPrivateRegistryDiff, diffSecret, diffSecretVariableVisibility), + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + Description: "The auto-generated name of the private registry (computed by GitHub).", + }, + "registry_type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"maven_repository", "nuget_feed", "goproxy_server", "npm_registry", "rubygems_server", "cargo_registry", "composer_repository", "docker_registry", "git_source", "helm_registry", "pub_repository", "python_index", "terraform_registry"}, false)), + Description: "The registry type.", + }, + "url": { + Type: schema.TypeString, + Required: true, + Description: "The URL of the private registry.", + }, + "username": { + Type: schema.TypeString, + Optional: true, + Description: "The username to use when authenticating with the private registry.", + }, + "replaces_base": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Indicates whether this private registry should replace the base registry.", + }, + "value": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + ConflictsWith: []string{"value_encrypted"}, + Description: "The plaintext secret to be encrypted and sent to GitHub. This is used for a token when auth_type is token, and for a password when auth_type is username_password. Required when auth_type is token or username_password.", + }, + "value_encrypted": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + ConflictsWith: []string{"value"}, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsBase64), + Description: "The encrypted value of the secret using the GitHub public key in Base64 format.", + }, + "key_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + RequiredWith: []string{"value_encrypted"}, + ConflictsWith: []string{"value"}, + Description: "ID of the public key used to encrypt the secret. Required if value_encrypted is set.", + }, + "visibility": { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"all", "private", "selected"}, false)), + Description: "Configures the access that repositories have to the organization private registry. Must be one of `all`, `private`, or `selected`.", + }, + "selected_repository_ids": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeInt, + }, + Description: "An array of repository IDs that can access the organization private registry.", + }, + "auth_type": { + Type: schema.TypeString, + Optional: true, + Default: "token", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"token", "username_password", "oidc_azure", "oidc_aws", "oidc_jfrog"}, false)), + Description: "The authentication type for the private registry. Can be `token`, `username_password`, `oidc_azure`, `oidc_aws`, or `oidc_jfrog`. Defaults to `token`.", + }, + "oidc_azure_tenant_id": { + Type: schema.TypeString, + Optional: true, + Description: "The tenant ID of the Azure AD application. Required when auth_type is oidc_azure.", + }, + "oidc_azure_client_id": { + Type: schema.TypeString, + Optional: true, + Description: "The client ID of the Azure AD application. Required when auth_type is oidc_azure.", + }, + "oidc_aws_region": { + Type: schema.TypeString, + Optional: true, + Description: "The AWS region. Required when auth_type is oidc_aws.", + }, + "oidc_aws_account_id": { + Type: schema.TypeString, + Optional: true, + Description: "The AWS account ID. Required when auth_type is oidc_aws.", + }, + "oidc_aws_role_name": { + Type: schema.TypeString, + Optional: true, + Description: "The AWS IAM role name. Required when auth_type is oidc_aws.", + }, + "oidc_aws_domain": { + Type: schema.TypeString, + Optional: true, + Description: "The CodeArtifact domain. Required when auth_type is oidc_aws.", + }, + "oidc_aws_domain_owner": { + Type: schema.TypeString, + Optional: true, + Description: "The CodeArtifact domain owner. Required when auth_type is oidc_aws.", + }, + "oidc_jfrog_provider_name": { + Type: schema.TypeString, + Optional: true, + Description: "The JFrog OIDC provider name. Required when auth_type is oidc_jfrog.", + }, + "oidc_audience": { + Type: schema.TypeString, + Optional: true, + Description: "The OIDC audience.", + }, + "oidc_jfrog_identity_mapping_name": { + Type: schema.TypeString, + Optional: true, + Description: "The JFrog identity mapping name.", + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + Description: "The timestamp when the private registry was created.", + }, + "updated_at": { + Type: schema.TypeString, + Computed: true, + Description: "The timestamp when the private registry was last updated.", + }, + }, + } +} + +func resourceGithubOrganizationPrivateRegistryDiff(_ context.Context, d *schema.ResourceDiff, _ any) error { + authType := d.Get("auth_type").(string) + if authType != "token" && authType != "username_password" { + return nil + } + + if _, ok := d.GetOk("value"); ok { + return nil + } + if _, ok := d.GetOk("value_encrypted"); ok { + return nil + } + + return fmt.Errorf("one of `value,value_encrypted` must be specified when auth_type is %q", authType) +} + +func resourceGithubOrganizationPrivateRegistryCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + org := meta.(*Owner).name + + encryptedValue := d.Get("value_encrypted").(string) + keyID := d.Get("key_id").(string) + + authType := d.Get("auth_type").(string) + if authType == "token" || authType == "username_password" { + if keyID == "" || len(encryptedValue) == 0 { + ki, pk, err := getOrganizationRegistryPublicKeyDetails(ctx, client, org) + if err != nil { + return diag.FromErr(err) + } + keyID = ki + + if len(encryptedValue) == 0 { + plaintextValue := d.Get("value").(string) + encryptedBytes, err := encryptPlaintext(plaintextValue, pk) + if err != nil { + return diag.FromErr(err) + } + encryptedValue = base64.StdEncoding.EncodeToString(encryptedBytes) + } + } + } + + payload := github.CreateOrganizationPrivateRegistry{ + RegistryType: github.PrivateRegistryType(d.Get("registry_type").(string)), + URL: d.Get("url").(string), + Visibility: github.PrivateRegistryVisibility(d.Get("visibility").(string)), + } + + if v, ok := d.GetOk("username"); ok { + payload.Username = new(v.(string)) + } + if v, ok := d.GetOk("replaces_base"); ok { + payload.ReplacesBase = new(v.(bool)) + } + if v, ok := d.GetOk("auth_type"); ok { + payload.AuthType = new(v.(string)) + } + + if encryptedValue != "" { + payload.EncryptedValue = new(encryptedValue) + payload.KeyID = new(keyID) + } + + if v, ok := d.GetOk("oidc_azure_tenant_id"); ok { + payload.TenantID = new(v.(string)) + } + if v, ok := d.GetOk("oidc_azure_client_id"); ok { + payload.ClientID = new(v.(string)) + } + if v, ok := d.GetOk("oidc_aws_region"); ok { + payload.AWSRegion = new(v.(string)) + } + if v, ok := d.GetOk("oidc_aws_account_id"); ok { + payload.AccountID = new(v.(string)) + } + if v, ok := d.GetOk("oidc_aws_role_name"); ok { + payload.RoleName = new(v.(string)) + } + if v, ok := d.GetOk("oidc_aws_domain"); ok { + payload.Domain = new(v.(string)) + } + if v, ok := d.GetOk("oidc_aws_domain_owner"); ok { + payload.DomainOwner = new(v.(string)) + } + if v, ok := d.GetOk("oidc_jfrog_provider_name"); ok { + payload.JFrogOIDCProviderName = new(v.(string)) + } + if v, ok := d.GetOk("oidc_audience"); ok { + payload.Audience = new(v.(string)) + } + if v, ok := d.GetOk("oidc_jfrog_identity_mapping_name"); ok { + payload.IdentityMappingName = new(v.(string)) + } + + if v, ok := d.GetOk("selected_repository_ids"); ok { + ids := v.(*schema.Set).List() + for _, id := range ids { + payload.SelectedRepositoryIDs = append(payload.SelectedRepositoryIDs, int64(id.(int))) + } + } + + registry, _, err := client.PrivateRegistries.CreateOrganizationPrivateRegistry(ctx, org, payload) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(registry.GetName()) + + return resourceGithubOrganizationPrivateRegistryRead(ctx, d, meta) +} + +func resourceGithubOrganizationPrivateRegistryRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + org := meta.(*Owner).name + + registry, _, err := client.PrivateRegistries.GetOrganizationPrivateRegistry(ctx, org, d.Id()) + if err != nil { + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) && ghErr.Response.StatusCode == 404 { + d.SetId("") + return nil + } + return diag.FromErr(err) + } + if err := d.Set("name", registry.GetName()); err != nil { + return diag.FromErr(err) + } + if registry.RegistryType != nil { + if err := d.Set("registry_type", string(*registry.RegistryType)); err != nil { + return diag.FromErr(err) + } + } + if err := d.Set("url", registry.GetURL()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("username", registry.GetUsername()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("replaces_base", registry.GetReplacesBase()); err != nil { + return diag.FromErr(err) + } + if registry.Visibility != nil { + if err := d.Set("visibility", string(*registry.Visibility)); err != nil { + return diag.FromErr(err) + } + } + + if registry.AuthType != nil { + if err := d.Set("auth_type", string(*registry.AuthType)); err != nil { + return diag.FromErr(err) + } + } + if err := d.Set("oidc_azure_tenant_id", registry.GetTenantID()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("oidc_azure_client_id", registry.GetClientID()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("oidc_aws_region", registry.GetAWSRegion()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("oidc_aws_account_id", registry.GetAccountID()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("oidc_aws_role_name", registry.GetRoleName()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("oidc_aws_domain", registry.GetDomain()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("oidc_aws_domain_owner", registry.GetDomainOwner()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("oidc_jfrog_provider_name", registry.GetJFrogOIDCProviderName()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("oidc_audience", registry.GetAudience()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("oidc_jfrog_identity_mapping_name", registry.GetIdentityMappingName()); err != nil { + return diag.FromErr(err) + } + + var repoIDs []any + for _, id := range registry.SelectedRepositoryIDs { + repoIDs = append(repoIDs, int(id)) + } + if err := d.Set("selected_repository_ids", schema.NewSet(schema.HashInt, repoIDs)); err != nil { + return diag.FromErr(err) + } + if err := d.Set("created_at", registry.GetCreatedAt().String()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("updated_at", registry.GetUpdatedAt().String()); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceGithubOrganizationPrivateRegistryUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + org := meta.(*Owner).name + + encryptedValue := d.Get("value_encrypted").(string) + keyID := d.Get("key_id").(string) + + authType := d.Get("auth_type").(string) + if (d.HasChange("value") || d.HasChange("value_encrypted")) && (authType == "token" || authType == "username_password") { + ki, pk, err := getOrganizationRegistryPublicKeyDetails(ctx, client, org) + if err != nil { + return diag.FromErr(err) + } + keyID = ki + + if len(encryptedValue) == 0 { + plaintextValue := d.Get("value").(string) + encryptedBytes, err := encryptPlaintext(plaintextValue, pk) + if err != nil { + return diag.FromErr(err) + } + encryptedValue = base64.StdEncoding.EncodeToString(encryptedBytes) + } + } + + payload := github.UpdateOrganizationPrivateRegistry{ + RegistryType: (*github.PrivateRegistryType)(new(d.Get("registry_type").(string))), + } + + if d.HasChange("url") { + payload.URL = new(d.Get("url").(string)) + } + if d.HasChange("username") { + payload.Username = new(d.Get("username").(string)) + } + if d.HasChange("replaces_base") { + payload.ReplacesBase = new(d.Get("replaces_base").(bool)) + } + if d.HasChange("visibility") { + payload.Visibility = (*github.PrivateRegistryVisibility)(new(d.Get("visibility").(string))) + } + if d.HasChange("auth_type") { + payload.AuthType = new(d.Get("auth_type").(string)) + } + + if encryptedValue != "" { + payload.EncryptedValue = new(encryptedValue) + payload.KeyID = new(keyID) + } + + if d.HasChange("oidc_azure_tenant_id") { + payload.TenantID = new(d.Get("oidc_azure_tenant_id").(string)) + } + if d.HasChange("oidc_azure_client_id") { + payload.ClientID = new(d.Get("oidc_azure_client_id").(string)) + } + if d.HasChange("oidc_aws_region") { + payload.AWSRegion = new(d.Get("oidc_aws_region").(string)) + } + if d.HasChange("oidc_aws_account_id") { + payload.AccountID = new(d.Get("oidc_aws_account_id").(string)) + } + if d.HasChange("oidc_aws_role_name") { + payload.RoleName = new(d.Get("oidc_aws_role_name").(string)) + } + if d.HasChange("oidc_aws_domain") { + payload.Domain = new(d.Get("oidc_aws_domain").(string)) + } + if d.HasChange("oidc_aws_domain_owner") { + payload.DomainOwner = new(d.Get("oidc_aws_domain_owner").(string)) + } + if d.HasChange("oidc_jfrog_provider_name") { + payload.JFrogOIDCProviderName = new(d.Get("oidc_jfrog_provider_name").(string)) + } + if d.HasChange("oidc_audience") { + payload.Audience = new(d.Get("oidc_audience").(string)) + } + if d.HasChange("oidc_jfrog_identity_mapping_name") { + payload.IdentityMappingName = new(d.Get("oidc_jfrog_identity_mapping_name").(string)) + } + if d.HasChange("selected_repository_ids") { + v := d.Get("selected_repository_ids").(*schema.Set).List() + var ids []int64 + for _, id := range v { + ids = append(ids, int64(id.(int))) + } + payload.SelectedRepositoryIDs = ids + } + + _, err := client.PrivateRegistries.UpdateOrganizationPrivateRegistry(ctx, org, d.Id(), payload) + if err != nil { + return diag.FromErr(err) + } + + return resourceGithubOrganizationPrivateRegistryRead(ctx, d, meta) +} + +func resourceGithubOrganizationPrivateRegistryDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + org := meta.(*Owner).name + + _, err := client.PrivateRegistries.DeleteOrganizationPrivateRegistry(ctx, org, d.Id()) + if err != nil { + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) && ghErr.Response.StatusCode == 404 { + return nil + } + + return diag.FromErr(err) + } + + return nil +} + +func getOrganizationRegistryPublicKeyDetails(ctx context.Context, client *github.Client, org string) (string, string, error) { + publicKey, _, err := client.PrivateRegistries.GetOrganizationPrivateRegistriesPublicKey(ctx, org) + if err != nil { + return "", "", err + } + return publicKey.GetKeyID(), publicKey.GetKey(), nil +} diff --git a/github/resource_github_organization_private_registry_test.go b/github/resource_github_organization_private_registry_test.go new file mode 100644 index 0000000000..5e50e10a38 --- /dev/null +++ b/github/resource_github_organization_private_registry_test.go @@ -0,0 +1,65 @@ +package github + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestAccGithubOrganizationPrivateRegistry(t *testing.T) { + configTmpl := ` + resource "github_organization_private_registry" "test" { + registry_type = "npm_registry" + url = "%s" + username = "github-actions" + value = "super_secret_token_123" + visibility = "%s" + } + + data "github_organization_private_registry" "test" { + name = github_organization_private_registry.test.name + } + ` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + skipUnlessMode(t, organization) + skipUnlessHasOrgs(t) + }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(configTmpl, "https://npm.pkg.github.com", "private"), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_organization_private_registry.test", tfjsonpath.New("name"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("github_organization_private_registry.test", tfjsonpath.New("registry_type"), knownvalue.StringExact("npm_registry")), + statecheck.ExpectKnownValue("github_organization_private_registry.test", tfjsonpath.New("url"), knownvalue.StringExact("https://npm.pkg.github.com")), + statecheck.ExpectKnownValue("github_organization_private_registry.test", tfjsonpath.New("username"), knownvalue.StringExact("github-actions")), + statecheck.ExpectKnownValue("github_organization_private_registry.test", tfjsonpath.New("visibility"), knownvalue.StringExact("private")), + statecheck.ExpectKnownValue("data.github_organization_private_registry.test", tfjsonpath.New("registry_type"), knownvalue.StringExact("npm_registry")), + }, + }, + { + ResourceName: "github_organization_private_registry.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"value"}, + }, + { + Config: fmt.Sprintf(configTmpl, "https://npm-registry.example.com", "all"), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_organization_private_registry.test", tfjsonpath.New("name"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("github_organization_private_registry.test", tfjsonpath.New("registry_type"), knownvalue.StringExact("npm_registry")), + statecheck.ExpectKnownValue("github_organization_private_registry.test", tfjsonpath.New("url"), knownvalue.StringExact("https://npm-registry.example.com")), + statecheck.ExpectKnownValue("github_organization_private_registry.test", tfjsonpath.New("username"), knownvalue.StringExact("github-actions")), + statecheck.ExpectKnownValue("github_organization_private_registry.test", tfjsonpath.New("visibility"), knownvalue.StringExact("all")), + statecheck.ExpectKnownValue("data.github_organization_private_registry.test", tfjsonpath.New("registry_type"), knownvalue.StringExact("npm_registry")), + }, + }, + }, + }) +}