diff --git a/docs/resources/gpg_key.md b/docs/resources/gpg_key.md new file mode 100644 index 00000000..6daa11e3 --- /dev/null +++ b/docs/resources/gpg_key.md @@ -0,0 +1,75 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "argocd_gpg_key Resource - terraform-provider-argocd" +subcategory: "" +description: |- + Manages GPG keys https://argo-cd.readthedocs.io/en/stable/user-guide/gpg-verification/ within ArgoCD. +--- + +# argocd_gpg_key (Resource) + +Manages [GPG keys](https://argo-cd.readthedocs.io/en/stable/user-guide/gpg-verification/) within ArgoCD. + +## Example Usage + +```terraform +resource "argocd_gpg_key" "this" { + public_key = < +## Schema + +### Required + +- `public_key` (String) Raw key data of the GPG key to create + +### Read-Only + +- `fingerprint` (String) Fingerprint is the fingerprint of the key +- `id` (String) GPG key identifier +- `owner` (String) Owner holds the owner identification, e.g. a name and e-mail address +- `sub_type` (String) SubType holds the key's sub type (e.g. rsa4096) +- `trust` (String) Trust holds the level of trust assigned to this key + +## Import + +Import is supported using the following syntax: + +```shell +# GPG Keys can be imported using the key ID. + +# Example: +terraform import argocd_gpg_key.this 9AD92955401D388D +``` diff --git a/examples/resources/argocd_gpg_key/import.sh b/examples/resources/argocd_gpg_key/import.sh new file mode 100644 index 00000000..e87e9b00 --- /dev/null +++ b/examples/resources/argocd_gpg_key/import.sh @@ -0,0 +1,4 @@ +# GPG Keys can be imported using the key ID. + +# Example: +terraform import argocd_gpg_key.this 9AD92955401D388D \ No newline at end of file diff --git a/examples/resources/argocd_gpg_key/resource.tf b/examples/resources/argocd_gpg_key/resource.tf new file mode 100644 index 00000000..c48e0586 --- /dev/null +++ b/examples/resources/argocd_gpg_key/resource.tf @@ -0,0 +1,33 @@ +resource "argocd_gpg_key" "this" { + public_key = <"), + resource.TestCheckResourceAttr("argocd_gpg_key.this", "sub_type", "rsa4096"), + resource.TestCheckResourceAttr("argocd_gpg_key.this", "trust", "unknown"), + ), + }, + // ImportState testing + { + ResourceName: "argocd_gpg_key.this", + ImportState: true, + ImportStateVerify: true, + }, + // Update (i.e. recreate) + { + Config: ` +resource "argocd_gpg_key" "this" { + public_key = <"), + resource.TestCheckResourceAttr("argocd_gpg_key.this", "sub_type", "rsa4096"), + resource.TestCheckResourceAttr("argocd_gpg_key.this", "trust", "unknown"), + ), + }, + }, + }) +} + +func TestAccArgoCDGPGKeyResource_Invalid_NotAGPGKey(t *testing.T) { + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: ` +resource "argocd_gpg_key" "invalid" { + public_key = "invalid" +} + `, + ExpectError: regexp.MustCompile("Invalid PGP Public Key"), + }, + }, + }) +} diff --git a/internal/provider/server_interface.go b/internal/provider/server_interface.go index e2c3eb3e..58bbf081 100644 --- a/internal/provider/server_interface.go +++ b/internal/provider/server_interface.go @@ -14,6 +14,7 @@ import ( "github.com/argoproj/argo-cd/v2/pkg/apiclient/applicationset" "github.com/argoproj/argo-cd/v2/pkg/apiclient/certificate" "github.com/argoproj/argo-cd/v2/pkg/apiclient/cluster" + "github.com/argoproj/argo-cd/v2/pkg/apiclient/gpgkey" "github.com/argoproj/argo-cd/v2/pkg/apiclient/project" "github.com/argoproj/argo-cd/v2/pkg/apiclient/repocreds" "github.com/argoproj/argo-cd/v2/pkg/apiclient/repository" @@ -37,6 +38,7 @@ type ServerInterface struct { ApplicationSetClient applicationset.ApplicationSetServiceClient CertificateClient certificate.CertificateServiceClient ClusterClient cluster.ClusterServiceClient + GPGKeysClient gpgkey.GPGKeyServiceClient ProjectClient project.ProjectServiceClient RepoCredsClient repocreds.RepoCredsServiceClient RepositoryClient repository.RepositoryServiceClient @@ -101,6 +103,11 @@ func (si *ServerInterface) InitClients(ctx context.Context) diag.Diagnostics { diags.Append(diagnostics.Error("failed to initialize cluster client", err)...) } + _, si.GPGKeysClient, err = ac.NewGPGKeyClient() + if err != nil { + diags.Append(diagnostics.Error("failed to initialize GPG keys client", err)...) + } + _, si.ProjectClient, err = ac.NewProjectClient() if err != nil { diags.Append(diagnostics.Error("failed to initialize project client", err)...) diff --git a/internal/sync/mutex.go b/internal/sync/mutex.go new file mode 100644 index 00000000..eb24f6db --- /dev/null +++ b/internal/sync/mutex.go @@ -0,0 +1,7 @@ +package sync + +import "sync" + +// GPGKeysMutex is used to handle concurrent access to ArgoCD GPG keys which are +// stored in the `argocd-gpg-keys-cm` ConfigMap resource +var GPGKeysMutex = &sync.RWMutex{} diff --git a/internal/types/pgp_public_key.go b/internal/types/pgp_public_key.go new file mode 100644 index 00000000..070aef31 --- /dev/null +++ b/internal/types/pgp_public_key.go @@ -0,0 +1,285 @@ +package types + +import ( + "context" + "fmt" + "strings" + + "github.com/ProtonMail/gopenpgp/v2/crypto" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/attr/xattr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +type pgpPublicKeyType uint8 + +const ( + PGPPublicKeyType pgpPublicKeyType = iota +) + +var ( + _ xattr.TypeWithValidate = PGPPublicKeyType + _ basetypes.StringTypable = PGPPublicKeyType + + _ basetypes.StringValuable = PGPPublicKey{} + _ basetypes.StringValuableWithSemanticEquals = PGPPublicKey{} +) + +// TerraformType returns the tftypes.Type that should be used to represent this +// framework type. +func (t pgpPublicKeyType) TerraformType(_ context.Context) tftypes.Type { + return tftypes.String +} + +// ValueFromString returns a StringValuable type given a StringValue. +func (t pgpPublicKeyType) ValueFromString(_ context.Context, in types.String) (basetypes.StringValuable, diag.Diagnostics) { + if in.IsUnknown() { + return PGPPublicKeyUnknown(), nil + } + + if in.IsNull() { + return PGPPublicKeyNull(), nil + } + + return PGPPublicKey{ + state: attr.ValueStateKnown, + value: in.ValueString(), + }, nil +} + +// ValueFromTerraform returns a Value given a tftypes.Value. This is meant to +// convert the tftypes.Value into a more convenient Go type for the provider to +// consume the data with. +func (t pgpPublicKeyType) ValueFromTerraform(_ context.Context, in tftypes.Value) (attr.Value, error) { + if !in.IsKnown() { + return PGPPublicKeyUnknown(), nil + } + + if in.IsNull() { + return PGPPublicKeyNull(), nil + } + + var s string + err := in.As(&s) + + if err != nil { + return nil, err + } + + return PGPPublicKey{ + state: attr.ValueStateKnown, + value: s, + }, nil +} + +// ValueType returns the Value type. +func (t pgpPublicKeyType) ValueType(context.Context) attr.Value { + return PGPPublicKey{} +} + +// Equal returns true if `o` is also a PGPPublicKeyType. +func (t pgpPublicKeyType) Equal(o attr.Type) bool { + _, ok := o.(pgpPublicKeyType) + return ok +} + +// ApplyTerraform5AttributePathStep applies the given AttributePathStep to the +// type. +func (t pgpPublicKeyType) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return nil, fmt.Errorf("cannot apply AttributePathStep %T to %s", step, t.String()) +} + +// String returns a human-friendly description of the PGPPublicKeyType. +func (t pgpPublicKeyType) String() string { + return "types.PGPPublicKeyType" +} + +// Validate implements type validation. +func (t pgpPublicKeyType) Validate(ctx context.Context, in tftypes.Value, path path.Path) diag.Diagnostics { + var diags diag.Diagnostics + + if !in.Type().Is(tftypes.String) { + diags.AddAttributeError( + path, + "PGPPublicKey Type Validation Error", + "An unexpected error was encountered trying to validate an attribute value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Expected String value, received %T with value: %v", in, in), + ) + + return diags + } + + if !in.IsKnown() || in.IsNull() { + return diags + } + + var value string + + err := in.As(&value) + if err != nil { + diags.AddAttributeError( + path, + "PGPPublicKey Type Validation Error", + "An unexpected error was encountered trying to validate an attribute value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Error: %s", err), + ) + + return diags + } + + _, err = crypto.NewKeyFromArmored(value) + if err != nil { + diags.AddAttributeError( + path, + "Invalid PGP Public Key", + err.Error()) + + return diags + } + + return diags +} + +func (t pgpPublicKeyType) Description() string { + return `PGP Public key in ASCII-armor base64 encoded format.` +} + +func PGPPublicKeyNull() PGPPublicKey { + return PGPPublicKey{ + state: attr.ValueStateNull, + } +} + +func PGPPublicKeyUnknown() PGPPublicKey { + return PGPPublicKey{ + state: attr.ValueStateUnknown, + } +} + +func PGPPublicKeyValue(value string) PGPPublicKey { + return PGPPublicKey{ + state: attr.ValueStateKnown, + value: value, + } +} + +type PGPPublicKey struct { + // state represents whether the value is null, unknown, or known. The + // zero-value is null. + state attr.ValueState + + // value contains the original string representation. + value string +} + +// Type returns a PGPPublicKeyType. +func (k PGPPublicKey) Type(_ context.Context) attr.Type { + return PGPPublicKeyType +} + +// ToStringValue should convert the value type to a String. +func (k PGPPublicKey) ToStringValue(ctx context.Context) (types.String, diag.Diagnostics) { + switch k.state { + case attr.ValueStateKnown: + return types.StringValue(k.value), nil + case attr.ValueStateNull: + return types.StringNull(), nil + case attr.ValueStateUnknown: + return types.StringUnknown(), nil + default: + return types.StringUnknown(), diag.Diagnostics{ + diag.NewErrorDiagnostic(fmt.Sprintf("unhandled PGPPublicKey state in ToStringValue: %s", k.state), ""), + } + } +} + +// ToTerraformValue returns the data contained in the *String as a string. If +// Unknown is true, it returns a tftypes.UnknownValue. If Null is true, it +// returns nil. +func (k PGPPublicKey) ToTerraformValue(ctx context.Context) (tftypes.Value, error) { + t := PGPPublicKeyType.TerraformType(ctx) + + switch k.state { + case attr.ValueStateKnown: + if err := tftypes.ValidateValue(t, k.value); err != nil { + return tftypes.NewValue(t, tftypes.UnknownValue), err + } + + return tftypes.NewValue(t, k.value), nil + case attr.ValueStateNull: + return tftypes.NewValue(t, nil), nil + case attr.ValueStateUnknown: + return tftypes.NewValue(t, tftypes.UnknownValue), nil + default: + return tftypes.NewValue(t, tftypes.UnknownValue), fmt.Errorf("unhandled PGPPublicKey state in ToTerraformValue: %s", k.state) + } +} + +// Equal returns true if `other` is a *PGPPublicKey and has the same value as `d`. +func (k PGPPublicKey) Equal(other attr.Value) bool { + o, ok := other.(PGPPublicKey) + + if !ok { + return false + } + + if k.state != o.state { + return false + } + + if k.state != attr.ValueStateKnown { + return true + } + + return k.value == o.value +} + +// IsNull returns true if the Value is not set, or is explicitly set to null. +func (k PGPPublicKey) IsNull() bool { + return k.state == attr.ValueStateNull +} + +// IsUnknown returns true if the Value is not yet known. +func (k PGPPublicKey) IsUnknown() bool { + return k.state == attr.ValueStateUnknown +} + +// String returns a summary representation of either the underlying Value, +// or UnknownValueString (``) when IsUnknown() returns true, +// or NullValueString (``) when IsNull() return true. +// +// This is an intentionally lossy representation, that are best suited for +// logging and error reporting, as they are not protected by +// compatibility guarantees within the framework. +func (k PGPPublicKey) String() string { + if k.IsUnknown() { + return attr.UnknownValueString + } + + if k.IsNull() { + return attr.NullValueString + } + + return k.value +} + +// ValuePGPPublicKey returns the known string value. If PGPPublicKey is null or unknown, returns "". +func (k PGPPublicKey) ValuePGPPublicKey() string { + return k.value +} + +// StringSemanticEquals should return true if the given value is +// semantically equal to the current value. This logic is used to prevent +// Terraform data consistency errors and resource drift where a value change +// may have inconsequential differences, such as spacing character removal +// in JSON formatted strings. +// +// Only known values are compared with this method as changing a value's +// state implicitly represents a different value. +func (k PGPPublicKey) StringSemanticEquals(ctx context.Context, other basetypes.StringValuable) (bool, diag.Diagnostics) { + return strings.TrimSpace(k.value) == strings.TrimSpace(other.String()), nil +} diff --git a/manifests/local-dev/gpg-key.tf b/manifests/local-dev/gpg-key.tf new file mode 100644 index 00000000..c48e0586 --- /dev/null +++ b/manifests/local-dev/gpg-key.tf @@ -0,0 +1,33 @@ +resource "argocd_gpg_key" "this" { + public_key = <