From 752dead82a067bfa364df81fc0134dcdb261340b Mon Sep 17 00:00:00 2001 From: MrLuje Date: Fri, 1 Jul 2022 11:19:19 +0200 Subject: [PATCH] feat: add resource argocd_repository_certificate (#185) * feat: add resource argocd_certificate (ssh) * feat: https certificate + explicit ssh/https first-level resource property * RetryContext around repository creation in case of we just created a repository certificate * feat: add feature check for RepositoryCertificates * docs: argocd_certificate * test: more argocd_certificate * fix: remove argocd_certificate import since it cannot be updated * fix: guard against already existing https cert * rename resource argocd_certificate to argocd_repository_certificate * fix: race-condition in resourceArgoCDRepositoryCertificates existing check --- argocd/features.go | 12 + argocd/provider.go | 1 + argocd/resource_argocd_repository.go | 62 +-- ...resource_argocd_repository_certificates.go | 366 ++++++++++++++ ...rce_argocd_repository_certificates_test.go | 469 ++++++++++++++++++ argocd/schema_repository_certificates.go | 81 +++ argocd/structure_repository_certificate.go | 87 ++++ argocd/validators.go | 5 +- docs/resources/repository_certificate.md | 64 +++ 9 files changed, 1114 insertions(+), 33 deletions(-) create mode 100644 argocd/resource_argocd_repository_certificates.go create mode 100644 argocd/resource_argocd_repository_certificates_test.go create mode 100644 argocd/schema_repository_certificates.go create mode 100644 argocd/structure_repository_certificate.go create mode 100644 docs/resources/repository_certificate.md diff --git a/argocd/features.go b/argocd/features.go index 23bcff1f..e8892135 100644 --- a/argocd/features.go +++ b/argocd/features.go @@ -8,6 +8,7 @@ import ( "github.com/Masterminds/semver" "github.com/argoproj/argo-cd/v2/pkg/apiclient" "github.com/argoproj/argo-cd/v2/pkg/apiclient/application" + "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/project" "github.com/argoproj/argo-cd/v2/pkg/apiclient/repocreds" @@ -25,6 +26,7 @@ const ( featureTokenIDs featureProjectScopedClusters featureClusterMetadata + featureRepositoryCertificates ) var ( @@ -35,12 +37,14 @@ var ( featureTokenIDs: semver.MustParse("1.5.3"), featureProjectScopedClusters: semver.MustParse("2.2.0"), featureClusterMetadata: semver.MustParse("2.2.0"), + featureRepositoryCertificates: semver.MustParse("1.2.0"), } ) type ServerInterface struct { ApiClient *apiclient.Client ApplicationClient *application.ApplicationServiceClient + CertificateClient *certificate.CertificateServiceClient ClusterClient *cluster.ClusterServiceClient ProjectClient *project.ProjectServiceClient RepositoryClient *repository.RepositoryServiceClient @@ -79,6 +83,14 @@ func (p *ServerInterface) initClients() error { p.ClusterClient = &clusterClient } + if p.CertificateClient == nil { + _, certClient, err := (*p.ApiClient).NewCertClient() + if err != nil { + return err + } + p.CertificateClient = &certClient + } + if p.ApplicationClient == nil { _, applicationClient, err := (*p.ApiClient).NewApplicationClient() if err != nil { diff --git a/argocd/provider.go b/argocd/provider.go index d9b00937..3a061d89 100644 --- a/argocd/provider.go +++ b/argocd/provider.go @@ -156,6 +156,7 @@ func Provider() *schema.Provider { ResourcesMap: map[string]*schema.Resource{ "argocd_application": resourceArgoCDApplication(), + "argocd_repository_certificate": resourceArgoCDRepositoryCertificates(), "argocd_cluster": resourceArgoCDCluster(), "argocd_project": resourceArgoCDProject(), "argocd_project_token": resourceArgoCDProjectToken(), diff --git a/argocd/resource_argocd_repository.go b/argocd/resource_argocd_repository.go index 2702f275..ebac0422 100644 --- a/argocd/resource_argocd_repository.go +++ b/argocd/resource_argocd_repository.go @@ -3,11 +3,13 @@ package argocd import ( "context" "fmt" + "regexp" "strings" "github.com/argoproj/argo-cd/v2/pkg/apiclient/repository" application "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -40,46 +42,44 @@ func resourceArgoCDRepositoryCreate(ctx context.Context, d *schema.ResourceData, c := *server.RepositoryClient repo := expandRepository(d) - tokenMutexConfiguration.Lock() - r, err := c.CreateRepository( - ctx, - &repository.RepoCreateRequest{ - Repo: repo, - Upsert: false, - }, - ) - tokenMutexConfiguration.Unlock() - - if err != nil { - return []diag.Diagnostic{ - { - Severity: diag.Error, - Summary: fmt.Sprintf("Repository %s not found", repo.Repo), - Detail: err.Error(), + err := resource.RetryContext(ctx, d.Timeout(schema.TimeoutCreate), func() *resource.RetryError { + tokenMutexConfiguration.Lock() + r, err := c.CreateRepository( + ctx, + &repository.RepoCreateRequest{ + Repo: repo, + Upsert: false, }, + ) + tokenMutexConfiguration.Unlock() + + if err != nil { + // TODO: better way to detect ssh handshake failing ? + if matched, _ := regexp.MatchString("ssh: handshake failed: knownhosts: key is unknown", err.Error()); matched { + return resource.RetryableError(fmt.Errorf("Hanshake failed for repository %s, retrying in case a repository certificate has been set recently", repo.Repo)) + } + return resource.NonRetryableError(fmt.Errorf("Repository %s not found: %s", repo.Repo, err)) } - } - if r == nil { - return []diag.Diagnostic{ - { - Severity: diag.Error, - Summary: fmt.Sprintf("ArgoCD did not return an error or a repository result"), - }, + if r == nil { + return resource.NonRetryableError(fmt.Errorf("ArgoCD did not return an error or a repository result: %s", err)) } - } - if r.ConnectionState.Status == application.ConnectionStatusFailed { + if r.ConnectionState.Status == application.ConnectionStatusFailed { + return resource.NonRetryableError(fmt.Errorf("could not connect to repository %s: %s", repo.Repo, r.ConnectionState.Message)) + } + d.SetId(r.Repo) + return nil + }) + + if err != nil { return []diag.Diagnostic{ { Severity: diag.Error, - Summary: fmt.Sprintf( - "could not connect to repository %s: %s", - repo.Repo, - r.ConnectionState.Message, - ), + Summary: fmt.Sprintf("Error while creating repository %s", repo.Name), + Detail: err.Error(), }, } } - d.SetId(r.Repo) + return resourceArgoCDRepositoryRead(ctx, d, meta) } diff --git a/argocd/resource_argocd_repository_certificates.go b/argocd/resource_argocd_repository_certificates.go new file mode 100644 index 00000000..754f5fb0 --- /dev/null +++ b/argocd/resource_argocd_repository_certificates.go @@ -0,0 +1,366 @@ +package argocd + +import ( + "context" + "fmt" + "strings" + + "github.com/argoproj/argo-cd/v2/pkg/apiclient/certificate" + application "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceArgoCDRepositoryCertificates() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceArgoCDRepositoryCertificatesCreate, + ReadContext: resourceArgoCDRepositoryCertificatesRead, + DeleteContext: resourceArgoCDRepositoryCertificatesDelete, + Schema: certificatesSchema(), + } +} + +func resourceArgoCDRepositoryCertificatesCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + repoCertificate, err := expandRepositoryCertificate(d) + if err != nil { + return []diag.Diagnostic{ + { + Severity: diag.Error, + Summary: fmt.Sprintf("certificate %s could not be created", d.Id()), + Detail: err.Error(), + }, + } + } + + server := meta.(*ServerInterface) + if err := server.initClients(); err != nil { + return []diag.Diagnostic{ + { + Severity: diag.Error, + Summary: fmt.Sprintf("Failed to init clients"), + Detail: err.Error(), + }, + } + } + + featureRepositoryCertificateSupported, err := server.isFeatureSupported(featureRepositoryCertificates) + if err != nil { + return []diag.Diagnostic{ + { + Severity: diag.Error, + Summary: "feature not supported", + Detail: err.Error(), + }, + } + } + + if !featureRepositoryCertificateSupported { + return []diag.Diagnostic{ + { + Severity: diag.Error, + Summary: fmt.Sprintf( + "repository certificate is only supported from ArgoCD %s onwards", + featureVersionConstraintsMap[featureRepositoryCertificates].String()), + }, + } + } + c := *server.CertificateClient + + // Not doing a RLock here because we can have a race-condition between the ListCertificates & CreateCertificate + tokenMutexConfiguration.Lock() + + if repoCertificate.CertType == "https" { + rcl, err := c.ListCertificates(ctx, &certificate.RepositoryCertificateQuery{ + HostNamePattern: repoCertificate.ServerName, + CertType: repoCertificate.CertType, + CertSubType: repoCertificate.CertSubType, + }) + + if err != nil { + tokenMutexConfiguration.Unlock() + return []diag.Diagnostic{ + { + Severity: diag.Error, + Summary: fmt.Sprintf("certificates for host %s could not be listed", repoCertificate.ServerName), + Detail: err.Error(), + }, + } + } + + if len(rcl.Items) > 0 { + tokenMutexConfiguration.Unlock() + return []diag.Diagnostic{ + { + Severity: diag.Error, + Summary: fmt.Sprintf("https certificate for '%s' already exist.", repoCertificate.ServerName), + }, + } + } + } + + certs := application.RepositoryCertificateList{ + Items: []application.RepositoryCertificate{ + *repoCertificate, + }, + } + + rc, err := c.CreateCertificate( + ctx, + &certificate.RepositoryCertificateCreateRequest{ + Certificates: &certs, + Upsert: false, + }, + ) + tokenMutexConfiguration.Unlock() + + if err != nil { + return []diag.Diagnostic{ + { + Severity: diag.Error, + Summary: fmt.Sprintf("certificate for repository %s could not be created", repoCertificate.ServerName), + Detail: err.Error(), + }, + } + } + + // TODO: upstream bug : if https certificate already exists, the response will be empty + // instead of erroring about missing upsert flag but since the call is success + // we assume everything went fine and get the id from the request + var resourceId string + if len(rc.Items) > 0 { + err, resourceId = getId(&rc.Items[0]) + } else { + err, resourceId = getId(repoCertificate) + } + + if err != nil { + return []diag.Diagnostic{ + { + Severity: diag.Error, + Summary: fmt.Sprintf("certificate for repository %s created but could not be handled", repoCertificate.ServerName), + Detail: err.Error(), + }, + } + } + d.SetId(resourceId) + return resourceArgoCDRepositoryCertificatesRead(context.WithValue(ctx, "cert_data", repoCertificate.CertData), d, meta) +} + +// Compute resource's id as : +// for ssh -> certType/certSubType/serverName +// for https -> certType/serverName +func getId(rc *application.RepositoryCertificate) (error, string) { + if rc.CertType == "ssh" { + if rc.CertSubType == "" || rc.ServerName == "" { + return fmt.Errorf("invalid certificate: %s %s %s", rc.CertType, rc.CertSubType, rc.ServerName), "" + } + return nil, fmt.Sprintf("%s/%s/%s", rc.CertType, rc.CertSubType, rc.ServerName) + } else { + if rc.ServerName == "" { + return fmt.Errorf("invalid certificate: %s %s", rc.CertType, rc.ServerName), "" + } + return nil, fmt.Sprintf("%s/%s", rc.CertType, rc.ServerName) + } +} + +// Get serverName/certType/certSubType from resource's id +func fromId(id string) (error, string, string, string) { + parts := strings.Split(id, "/") + if len(parts) < 2 { + return fmt.Errorf("Unknown certificate %s in state", id), "", "", "" + } + certType := parts[0] + if certType == "ssh" { + if len(parts) < 3 || parts[0] == "" || parts[1] == "" || parts[2] == "" { + return fmt.Errorf("Unknown certificate %s in state", id), "", "", "" + } + return nil, parts[0], parts[1], parts[2] + } else { + if len(parts) < 2 || parts[0] == "" || parts[1] == "" { + return fmt.Errorf("Unknown certificate %s in state", id), "", "", "" + } + return nil, parts[0], parts[1], "" + } +} + +func resourceArgoCDRepositoryCertificatesRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + server := meta.(*ServerInterface) + if err := server.initClients(); err != nil { + return []diag.Diagnostic{ + { + Severity: diag.Error, + Summary: fmt.Sprintf("Failed to init clients"), + Detail: err.Error(), + }, + } + } + + featureRepositoryCertificateSupported, err := server.isFeatureSupported(featureRepositoryCertificates) + if err != nil { + return []diag.Diagnostic{ + { + Severity: diag.Error, + Summary: "feature not supported", + Detail: err.Error(), + }, + } + } + + if !featureRepositoryCertificateSupported { + return []diag.Diagnostic{ + { + Severity: diag.Error, + Summary: fmt.Sprintf( + "repository certificate is only supported from ArgoCD %s onwards", + featureVersionConstraintsMap[featureRepositoryCertificates].String()), + }, + } + } + + c := *server.CertificateClient + repoCertificate := application.RepositoryCertificate{} + err, certType, certSubType, serverName := fromId(d.Id()) + if err != nil { + return []diag.Diagnostic{ + { + Severity: diag.Error, + Summary: fmt.Sprintf("Failed to parse certificate state"), + Detail: err.Error(), + }, + } + } + + tokenMutexConfiguration.RLock() + rcl, err := c.ListCertificates(ctx, &certificate.RepositoryCertificateQuery{ + HostNamePattern: serverName, + CertType: certType, + CertSubType: certSubType, + }) + tokenMutexConfiguration.RUnlock() + + if err != nil { + return []diag.Diagnostic{ + { + Severity: diag.Error, + Summary: fmt.Sprintf("certificates for host %s could not be listed", d.Id()), + Detail: err.Error(), + }, + } + } + if rcl == nil || len(rcl.Items) == 0 { + // Certificate have already been deleted in an out-of-band fashion + d.SetId("") + return nil + } + for i, _rc := range rcl.Items { + err, resourceId := getId(&_rc) + if err != nil { + return []diag.Diagnostic{ + { + Severity: diag.Error, + Summary: fmt.Sprintf("certificate for repository %s could not be handled", repoCertificate.ServerName), + Detail: err.Error(), + }, + } + } + if resourceId == d.Id() { + repoCertificate = _rc + break + } + // Certificate have already been deleted in an out-of-band fashion + if i == len(rcl.Items)-1 { + d.SetId("") + return nil + } + } + + err = flattenRepositoryCertificate(&repoCertificate, d, ctx) + if err != nil { + return []diag.Diagnostic{ + { + Severity: diag.Error, + Summary: fmt.Sprintf("certificate %s could not be flattened", d.Id()), + Detail: err.Error(), + }, + } + } + return nil +} + +func resourceArgoCDRepositoryCertificatesDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + server := meta.(*ServerInterface) + if err := server.initClients(); err != nil { + return []diag.Diagnostic{ + { + Severity: diag.Error, + Summary: fmt.Sprintf("Failed to init clients"), + Detail: err.Error(), + }, + } + } + + featureRepositoryCertificateSupported, err := server.isFeatureSupported(featureRepositoryCertificates) + if err != nil { + return []diag.Diagnostic{ + { + Severity: diag.Error, + Summary: "feature not supported", + Detail: err.Error(), + }, + } + } + + if !featureRepositoryCertificateSupported { + return []diag.Diagnostic{ + { + Severity: diag.Error, + Summary: fmt.Sprintf( + "repository certificate is only supported from ArgoCD %s onwards", + featureVersionConstraintsMap[featureRepositoryCertificates].String()), + }, + } + } + + c := *server.CertificateClient + err, certType, certSubType, serverName := fromId(d.Id()) + if err != nil { + return []diag.Diagnostic{ + { + Severity: diag.Error, + Summary: fmt.Sprintf("Failed to parse certificate state"), + Detail: err.Error(), + }, + } + } + + query := certificate.RepositoryCertificateQuery{ + HostNamePattern: serverName, + CertType: certType, + CertSubType: certSubType, + } + + tokenMutexConfiguration.Lock() + _, err = c.DeleteCertificate( + ctx, + &query, + ) + tokenMutexConfiguration.Unlock() + + if err != nil { + if strings.Contains(err.Error(), "NotFound") { + // Certificate have already been deleted in an out-of-band fashion + d.SetId("") + return nil + } else { + return []diag.Diagnostic{ + { + Severity: diag.Error, + Summary: fmt.Sprintf("certificate for repository %s could not be deleted", d.Id()), + Detail: err.Error(), + }, + } + } + } + d.SetId("") + return nil +} diff --git a/argocd/resource_argocd_repository_certificates_test.go b/argocd/resource_argocd_repository_certificates_test.go new file mode 100644 index 00000000..38c09af2 --- /dev/null +++ b/argocd/resource_argocd_repository_certificates_test.go @@ -0,0 +1,469 @@ +package argocd + +import ( + "fmt" + "os/exec" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/stretchr/testify/assert" +) + +func TestAccArgoCDRepositoryCertificatesSSH(t *testing.T) { + serverName := acctest.RandomWithPrefix("mywebsite") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccArgoCDRepositoryCertificatesSSH( + serverName, + "ecdsa-sha2-nistp256", + // gitlab's + "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=", + ), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("argocd_repository_certificate.simple", "ssh.0.server_name", serverName), + resource.TestCheckResourceAttr("argocd_repository_certificate.simple", "ssh.0.cert_subtype", "ecdsa-sha2-nistp256"), + resource.TestCheckResourceAttrSet("argocd_repository_certificate.simple", "ssh.0.cert_info"), + ), + }, + // same, no diff + { + Config: testAccArgoCDRepositoryCertificatesSSH( + serverName, + "ecdsa-sha2-nistp256", + // gitlab's + "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=", + ), + PlanOnly: true, + ExpectNonEmptyPlan: false, + }, + // change only the cert_data => same id => diff + { + Config: testAccArgoCDRepositoryCertificatesSSH( + serverName, + "ecdsa-sha2-nistp256", + // github's + "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=", + ), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + // change cert_subtype & cert_data => changes id => diff + { + Config: testAccArgoCDRepositoryCertificatesSSH( + serverName, + "ssh-rsa", + // github's + "AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==", + ), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("argocd_repository_certificate.simple", "ssh.0.server_name", serverName), + resource.TestCheckResourceAttr("argocd_repository_certificate.simple", "ssh.0.cert_subtype", "ssh-rsa"), + ), + }, + }, + }) +} + +func TestAccArgoCDRepositoryCertificatesHttps(t *testing.T) { + serverName := acctest.RandomWithPrefix("github") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccArgoCDRepositoryCertificateHttps( + serverName, + // github's + "-----BEGIN CERTIFICATE-----\nMIIFajCCBPCgAwIBAgIQBRiaVOvox+kD4KsNklVF3jAKBggqhkjOPQQDAzBWMQsw\nCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMTAwLgYDVQQDEydEaWdp\nQ2VydCBUTFMgSHlicmlkIEVDQyBTSEEzODQgMjAyMCBDQTEwHhcNMjIwMzE1MDAw\nMDAwWhcNMjMwMzE1MjM1OTU5WjBmMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2Fs\naWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEVMBMGA1UEChMMR2l0SHVi\nLCBJbmMuMRMwEQYDVQQDEwpnaXRodWIuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0D\nAQcDQgAESrCTcYUh7GI/y3TARsjnANwnSjJLitVRgwgRI1JlxZ1kdZQQn5ltP3v7\nKTtYuDdUeEu3PRx3fpDdu2cjMlyA0aOCA44wggOKMB8GA1UdIwQYMBaAFAq8CCkX\njKU5bXoOzjPHLrPt+8N6MB0GA1UdDgQWBBR4qnLGcWloFLVZsZ6LbitAh0I7HjAl\nBgNVHREEHjAcggpnaXRodWIuY29tgg53d3cuZ2l0aHViLmNvbTAOBgNVHQ8BAf8E\nBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMIGbBgNVHR8EgZMw\ngZAwRqBEoEKGQGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRMU0h5\nYnJpZEVDQ1NIQTM4NDIwMjBDQTEtMS5jcmwwRqBEoEKGQGh0dHA6Ly9jcmw0LmRp\nZ2ljZXJ0LmNvbS9EaWdpQ2VydFRMU0h5YnJpZEVDQ1NIQTM4NDIwMjBDQTEtMS5j\ncmwwPgYDVR0gBDcwNTAzBgZngQwBAgIwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3\ndy5kaWdpY2VydC5jb20vQ1BTMIGFBggrBgEFBQcBAQR5MHcwJAYIKwYBBQUHMAGG\nGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBPBggrBgEFBQcwAoZDaHR0cDovL2Nh\nY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VExTSHlicmlkRUNDU0hBMzg0MjAy\nMENBMS0xLmNydDAJBgNVHRMEAjAAMIIBfwYKKwYBBAHWeQIEAgSCAW8EggFrAWkA\ndgCt9776fP8QyIudPZwePhhqtGcpXc+xDCTKhYY069yCigAAAX+Oi8SRAAAEAwBH\nMEUCIAR9cNnvYkZeKs9JElpeXwztYB2yLhtc8bB0rY2ke98nAiEAjiML8HZ7aeVE\nP/DkUltwIS4c73VVrG9JguoRrII7gWMAdwA1zxkbv7FsV78PrUxtQsu7ticgJlHq\nP+Eq76gDwzvWTAAAAX+Oi8R7AAAEAwBIMEYCIQDNckqvBhup7GpANMf0WPueytL8\nu/PBaIAObzNZeNMpOgIhAMjfEtE6AJ2fTjYCFh/BNVKk1mkTwBTavJlGmWomQyaB\nAHYAs3N3B+GEUPhjhtYFqdwRCUp5LbFnDAuH3PADDnk2pZoAAAF/jovErAAABAMA\nRzBFAiEA9Uj5Ed/XjQpj/MxQRQjzG0UFQLmgWlc73nnt3CJ7vskCICqHfBKlDz7R\nEHdV5Vk8bLMBW1Q6S7Ga2SbFuoVXs6zFMAoGCCqGSM49BAMDA2gAMGUCMCiVhqft\n7L/stBmv1XqSRNfE/jG/AqKIbmjGTocNbuQ7kt1Cs7kRg+b3b3C9Ipu5FQIxAM7c\ntGKrYDGt0pH8iF6rzbp9Q4HQXMZXkNxg+brjWxnaOVGTDNwNH7048+s/hT9bUQ==\n-----END CERTIFICATE-----", + ), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("argocd_repository_certificate.simple", "https.0.server_name", serverName), + resource.TestCheckResourceAttr("argocd_repository_certificate.simple", "https.0.cert_subtype", "ecdsa"), + resource.TestCheckResourceAttrSet("argocd_repository_certificate.simple", "https.0.cert_info"), + ), + }, + { + Config: testAccArgoCDRepositoryCertificateHttps( + serverName, + // gitlab's + "-----BEGIN CERTIFICATE-----\nMIIGBzCCBO+gAwIBAgIQXCLSMilzZJR9TSABzbgKzzANBgkqhkiG9w0BAQsFADCB\njzELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\nA1UEBxMHU2FsZm9yZDEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQD\nEy5TZWN0aWdvIFJTQSBEb21haW4gVmFsaWRhdGlvbiBTZWN1cmUgU2VydmVyIENB\nMB4XDTIxMDQxMjAwMDAwMFoXDTIyMDUxMTIzNTk1OVowFTETMBEGA1UEAxMKZ2l0\nbGFiLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANXnhcvOl289\n8oMglaax6bDz988oNMpXZCH6sI7Fzx9G/isEPObN6cyP+fjFa0dvwRmOHnepk2eo\nbzcECdgdBLCa7E29p7lLF0NFFTuIb52ew58fK/209XJ3amvjJ/m5rPP00uHrT+9v\nky2jkQUQszuC9R4vK+tfs2S5z9w6qh3hwIJecChzWKce8hRZdiO9S7ix/6ZNiAgw\nY2h8AiG0VruPOJ6PbNXOFUTsajK0EP8AzJfNDIjvWHjUOawR352m4eKxXvXm9knd\nB/w1gY90jmAQ9JIiyOm+QlmHwO+qQUpWYOxt5Xnb0Pp/RRHEtxDgjygQWajAwsxG\nobx6sCf6+qcCAwEAAaOCAtYwggLSMB8GA1UdIwQYMBaAFI2MXsRUrYrhd+mb+ZsF\n4bgBjWHhMB0GA1UdDgQWBBTFjbuGoOUrgk9Dhr35DblkBZCj1jAOBgNVHQ8BAf8E\nBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUH\nAwIwSQYDVR0gBEIwQDA0BgsrBgEEAbIxAQICBzAlMCMGCCsGAQUFBwIBFhdodHRw\nczovL3NlY3RpZ28uY29tL0NQUzAIBgZngQwBAgEwgYQGCCsGAQUFBwEBBHgwdjBP\nBggrBgEFBQcwAoZDaHR0cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUlNBRG9t\nYWluVmFsaWRhdGlvblNlY3VyZVNlcnZlckNBLmNydDAjBggrBgEFBQcwAYYXaHR0\ncDovL29jc3Auc2VjdGlnby5jb20wggEEBgorBgEEAdZ5AgQCBIH1BIHyAPAAdgBG\npVXrdfqRIDC1oolp9PN9ESxBdL79SbiFq/L8cP5tRwAAAXjDcW8TAAAEAwBHMEUC\nIQCxf+r8/dbHJDrh0YTAKSwdR8VUxAT6kHN5/HLuOvSsKgIgY2jAAf/tr59/f0JX\nKvHaN4qIv54gtj+KsNo7N0d4xcEAdgDfpV6raIJPH2yt7rhfTj5a6s2iEqRqXo47\nEsAgRFwqcwAAAXjDcW7VAAAEAwBHMEUCID0jtWvtpO1yypP7i7SeZZb3dQ6QdLlD\nlXpvWhjqrQfdAiEA0gp8tTUwOt2XN01OVTUrDgb4wV5VbFtx1SSYNFREQxwweQYD\nVR0RBHIwcIIKZ2l0bGFiLmNvbYIPYXV0aC5naXRsYWIuY29tghRjdXN0b21lcnMu\nZ2l0bGFiLmNvbYIaZW1haWwuY3VzdG9tZXJzLmdpdGxhYi5jb22CD2dwcmQuZ2l0\nbGFiLmNvbYIOd3d3LmdpdGxhYi5jb20wDQYJKoZIhvcNAQELBQADggEBAD7lgx6z\ncZI+uLtr7fYWOZDtPChNy7YjAXVtDbrQ61D1lESUIZwyDF9/xCDMqMSe+It2+j+t\nT0PHkbz6zbJdUMQhQxW0RLMZUthPg66YLqRJuvBU7VdWHxhqjfFb9UZvxOzTGgmN\nMuzmdThtlhRacNCTxGO/AJfcAt13RbKyR30UtqHb883qAH6isQvYFsQmijXcJXiT\ntRbcJ1Dm/dI+57BCTYLp2WfBdg0Axla5QsApQ+ER5GZoY1m6H3+OWpX77IdCgXF+\nHMtKCn08QLVBjhLr3IkeKgrYJTR1IDmzRwGUuUVvn1iO9+W10GV02SMngdN4nFp3\nwoE3CsYogf1SfQM=\n-----END CERTIFICATE-----", + ), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("argocd_repository_certificate.simple", "https.0.server_name", serverName), + resource.TestCheckResourceAttr("argocd_repository_certificate.simple", "https.0.cert_subtype", "rsa"), + resource.TestCheckResourceAttrSet("argocd_repository_certificate.simple", "https.0.cert_info"), + ), + }, + }, + }) +} + +func TestAccArgoCDRepositoryCertificatesHttps_Crash(t *testing.T) { + serverName := acctest.RandomWithPrefix("github") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccArgoCDRepositoryCertificateHttps( + serverName, + // github's + "-----BEGIN CERTIFICATE-----\nMIIFajCCBPCgAwIBAgIQBRiaVOvox+kD4KsNklVF3jAKBggqhkjOPQQDAzBWMQsw\nCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMTAwLgYDVQQDEydEaWdp\nQ2VydCBUTFMgSHlicmlkIEVDQyBTSEEzODQgMjAyMCBDQTEwHhcNMjIwMzE1MDAw\nMDAwWhcNMjMwMzE1MjM1OTU5WjBmMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2Fs\naWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEVMBMGA1UEChMMR2l0SHVi\nLCBJbmMuMRMwEQYDVQQDEwpnaXRodWIuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0D\nAQcDQgAESrCTcYUh7GI/y3TARsjnANwnSjJLitVRgwgRI1JlxZ1kdZQQn5ltP3v7\nKTtYuDdUeEu3PRx3fpDdu2cjMlyA0aOCA44wggOKMB8GA1UdIwQYMBaAFAq8CCkX\njKU5bXoOzjPHLrPt+8N6MB0GA1UdDgQWBBR4qnLGcWloFLVZsZ6LbitAh0I7HjAl\nBgNVHREEHjAcggpnaXRodWIuY29tgg53d3cuZ2l0aHViLmNvbTAOBgNVHQ8BAf8E\nBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMIGbBgNVHR8EgZMw\ngZAwRqBEoEKGQGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRMU0h5\nYnJpZEVDQ1NIQTM4NDIwMjBDQTEtMS5jcmwwRqBEoEKGQGh0dHA6Ly9jcmw0LmRp\nZ2ljZXJ0LmNvbS9EaWdpQ2VydFRMU0h5YnJpZEVDQ1NIQTM4NDIwMjBDQTEtMS5j\ncmwwPgYDVR0gBDcwNTAzBgZngQwBAgIwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3\ndy5kaWdpY2VydC5jb20vQ1BTMIGFBggrBgEFBQcBAQR5MHcwJAYIKwYBBQUHMAGG\nGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBPBggrBgEFBQcwAoZDaHR0cDovL2Nh\nY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VExTSHlicmlkRUNDU0hBMzg0MjAy\nMENBMS0xLmNydDAJBgNVHRMEAjAAMIIBfwYKKwYBBAHWeQIEAgSCAW8EggFrAWkA\ndgCt9776fP8QyIudPZwePhhqtGcpXc+xDCTKhYY069yCigAAAX+Oi8SRAAAEAwBH\nMEUCIAR9cNnvYkZeKs9JElpeXwztYB2yLhtc8bB0rY2ke98nAiEAjiML8HZ7aeVE\nP/DkUltwIS4c73VVrG9JguoRrII7gWMAdwA1zxkbv7FsV78PrUxtQsu7ticgJlHq\nP+Eq76gDwzvWTAAAAX+Oi8R7AAAEAwBIMEYCIQDNckqvBhup7GpANMf0WPueytL8\nu/PBaIAObzNZeNMpOgIhAMjfEtE6AJ2fTjYCFh/BNVKk1mkTwBTavJlGmWomQyaB\nAHYAs3N3B+GEUPhjhtYFqdwRCUp5LbFnDAuH3PADDnk2pZoAAAF/jovErAAABAMA\nRzBFAiEA9Uj5Ed/XjQpj/MxQRQjzG0UFQLmgWlc73nnt3CJ7vskCICqHfBKlDz7R\nEHdV5Vk8bLMBW1Q6S7Ga2SbFuoVXs6zFMAoGCCqGSM49BAMDA2gAMGUCMCiVhqft\n7L/stBmv1XqSRNfE/jG/AqKIbmjGTocNbuQ7kt1Cs7kRg+b3b3C9Ipu5FQIxAM7c\ntGKrYDGt0pH8iF6rzbp9Q4HQXMZXkNxg+brjWxnaOVGTDNwNH7048+s/hT9bUQ==\n-----END CERTIFICATE-----", + ), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("argocd_repository_certificate.simple", "https.0.server_name", serverName), + resource.TestCheckResourceAttr("argocd_repository_certificate.simple", "https.0.cert_subtype", "ecdsa"), + resource.TestCheckResourceAttrSet("argocd_repository_certificate.simple", "https.0.cert_info"), + ), + }, + { + Config: testAccArgoCDRepositoryCertificateHttps( + serverName, + // gitlab's + "-----BEGIN CERTIFICATE-----\nMIIGBzCCBO+gAwIBAgIQXCLSMilzZJR9TSABzbgKzzANBgkqhkiG9w0BAQsFADCB\njzELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\nA1UEBxMHU2FsZm9yZDEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQD\nEy5TZWN0aWdvIFJTQSBEb21haW4gVmFsaWRhdGlvbiBTZWN1cmUgU2VydmVyIENB\nMB4XDTIxMDQxMjAwMDAwMFoXDTIyMDUxMTIzNTk1OVowFTETMBEGA1UEAxMKZ2l0\nbGFiLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANXnhcvOl289\n8oMglaax6bDz988oNMpXZCH6sI7Fzx9G/isEPObN6cyP+fjFa0dvwRmOHnepk2eo\nbzcECdgdBLCa7E29p7lLF0NFFTuIb52ew58fK/209XJ3amvjJ/m5rPP00uHrT+9v\nky2jkQUQszuC9R4vK+tfs2S5z9w6qh3hwIJecChzWKce8hRZdiO9S7ix/6ZNiAgw\nY2h8AiG0VruPOJ6PbNXOFUTsajK0EP8AzJfNDIjvWHjUOawR352m4eKxXvXm9knd\nB/w1gY90jmAQ9JIiyOm+QlmHwO+qQUpWYOxt5Xnb0Pp/RRHEtxDgjygQWajAwsxG\nobx6sCf6+qcCAwEAAaOCAtYwggLSMB8GA1UdIwQYMBaAFI2MXsRUrYrhd+mb+ZsF\n4bgBjWHhMB0GA1UdDgQWBBTFjbuGoOUrgk9Dhr35DblkBZCj1jAOBgNVHQ8BAf8E\nBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUH\nAwIwSQYDVR0gBEIwQDA0BgsrBgEEAbIxAQICBzAlMCMGCCsGAQUFBwIBFhdodHRw\nczovL3NlY3RpZ28uY29tL0NQUzAIBgZngQwBAgEwgYQGCCsGAQUFBwEBBHgwdjBP\nBggrBgEFBQcwAoZDaHR0cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUlNBRG9t\nYWluVmFsaWRhdGlvblNlY3VyZVNlcnZlckNBLmNydDAjBggrBgEFBQcwAYYXaHR0\ncDovL29jc3Auc2VjdGlnby5jb20wggEEBgorBgEEAdZ5AgQCBIH1BIHyAPAAdgBG\npVXrdfqRIDC1oolp9PN9ESxBdL79SbiFq/L8cP5tRwAAAXjDcW8TAAAEAwBHMEUC\nIQCxf+r8/dbHJDrh0YTAKSwdR8VUxAT6kHN5/HLuOvSsKgIgY2jAAf/tr59/f0JX\nKvHaN4qIv54gtj+KsNo7N0d4xcEAdgDfpV6raIJPH2yt7rhfTj5a6s2iEqRqXo47\nEsAgRFwqcwAAAXjDcW7VAAAEAwBHMEUCID0jtWvtpO1yypP7i7SeZZb3dQ6QdLlD\nlXpvWhjqrQfdAiEA0gp8tTUwOt2XN01OVTUrDgb4wV5VbFtx1SSYNFREQxwweQYD\nVR0RBHIwcIIKZ2l0bGFiLmNvbYIPYXV0aC5naXRsYWIuY29tghRjdXN0b21lcnMu\nZ2l0bGFiLmNvbYIaZW1haWwuY3VzdG9tZXJzLmdpdGxhYi5jb22CD2dwcmQuZ2l0\nbGFiLmNvbYIOd3d3LmdpdGxhYi5jb20wDQYJKoZIhvcNAQELBQADggEBAD7lgx6z\ncZI+uLtr7fYWOZDtPChNy7YjAXVtDbrQ61D1lESUIZwyDF9/xCDMqMSe+It2+j+t\nT0PHkbz6zbJdUMQhQxW0RLMZUthPg66YLqRJuvBU7VdWHxhqjfFb9UZvxOzTGgmN\nMuzmdThtlhRacNCTxGO/AJfcAt13RbKyR30UtqHb883qAH6isQvYFsQmijXcJXiT\ntRbcJ1Dm/dI+57BCTYLp2WfBdg0Axla5QsApQ+ER5GZoY1m6H3+OWpX77IdCgXF+\nHMtKCn08QLVBjhLr3IkeKgrYJTR1IDmzRwGUuUVvn1iO9+W10GV02SMngdN4nFp3\nwoE3CsYogf1SfQM=\n-----END CERTIFICATE-----", + ), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("argocd_repository_certificate.simple", "https.0.server_name", serverName), + resource.TestCheckResourceAttr("argocd_repository_certificate.simple", "https.0.cert_subtype", "rsa"), + resource.TestCheckResourceAttrSet("argocd_repository_certificate.simple", "https.0.cert_info"), + ), + }, + }, + }) +} + +func TestAccArgoCDRepositoryCertificatesSSH_Invalid(t *testing.T) { + certSubType := acctest.RandomWithPrefix("cert") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccArgoCDRepositoryCertificatesSSH( + "", + certSubType, + "", + ), + ExpectError: regexp.MustCompile("Invalid hostname in request"), + }, + { + Config: testAccArgoCDRepositoryCertificatesSSH( + "dummy_server", + certSubType, + "", + ), + ExpectError: regexp.MustCompile("invalid entry in known_hosts data"), + }, + }, + }) +} + +func TestAccArgoCDRepositoryCertificates_Empty(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccArgoCDRepositoryCertificates_Empty(), + ExpectError: regexp.MustCompile("one of `https,ssh` must be specified"), + }, + }, + }) +} + +func TestAccArgoCDRepositoryCertificatesSSH_Allow_Random_Subtype(t *testing.T) { + certSubType := acctest.RandomWithPrefix("cert") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccArgoCDRepositoryCertificatesSSH( + "dummy_server", + certSubType, + "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=", + ), + }, + }, + }) +} + +func TestAccArgoCDRepositoryCertificatesSSH_WithApplication(t *testing.T) { + appName := acctest.RandomWithPrefix("testacc") + + err, subtypesKeys := getSshKeysForHost("private-git-repository") + assert.NoError(t, err) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccArgoCDRepositoryCertificateCredentialsApplicationWithSSH(appName, subtypesKeys), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet( + "argocd_application.simple", + "metadata.0.uid", + ), + resource.TestCheckResourceAttr( + "argocd_repository.private", + "connection_state_status", + "Successful", + )), + }, + }, + }) +} + +func TestAccArgoCDRepositoryCertificatesSSH_CannotUpdateExisting(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccArgoCDRepositoryCertificatesSSH( + "github.com", + "ssh-rsa", + // github's + "AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==", + ), + ExpectError: regexp.MustCompile("already exist and upsert was not specified"), + }, + }, + }) +} + +func TestAccArgoCDRepositoryCertificatesSSH_CannotUpdateExisting_MultipleAtOnce(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccArgoCDRepositoryCertificateSSH_Duplicated( + "github.com", + "ssh-rsaaa", + // github's + "AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==", + ), + ExpectError: regexp.MustCompile("already exist and upsert was not specified"), + }, + }, + }) +} + +func TestAccArgoCDRepositoryCertificatesHttps_CannotUpdateExisting_MultipleAtOnce(t *testing.T) { + host := "github.com" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccArgoCDRepositoryCertificateHttps_Duplicated( + host, + // github's + "-----BEGIN CERTIFICATE-----\nMIIFajCCBPCgAwIBAgIQBRiaVOvox+kD4KsNklVF3jAKBggqhkjOPQQDAzBWMQsw\nCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMTAwLgYDVQQDEydEaWdp\nQ2VydCBUTFMgSHlicmlkIEVDQyBTSEEzODQgMjAyMCBDQTEwHhcNMjIwMzE1MDAw\nMDAwWhcNMjMwMzE1MjM1OTU5WjBmMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2Fs\naWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEVMBMGA1UEChMMR2l0SHVi\nLCBJbmMuMRMwEQYDVQQDEwpnaXRodWIuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0D\nAQcDQgAESrCTcYUh7GI/y3TARsjnANwnSjJLitVRgwgRI1JlxZ1kdZQQn5ltP3v7\nKTtYuDdUeEu3PRx3fpDdu2cjMlyA0aOCA44wggOKMB8GA1UdIwQYMBaAFAq8CCkX\njKU5bXoOzjPHLrPt+8N6MB0GA1UdDgQWBBR4qnLGcWloFLVZsZ6LbitAh0I7HjAl\nBgNVHREEHjAcggpnaXRodWIuY29tgg53d3cuZ2l0aHViLmNvbTAOBgNVHQ8BAf8E\nBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMIGbBgNVHR8EgZMw\ngZAwRqBEoEKGQGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRMU0h5\nYnJpZEVDQ1NIQTM4NDIwMjBDQTEtMS5jcmwwRqBEoEKGQGh0dHA6Ly9jcmw0LmRp\nZ2ljZXJ0LmNvbS9EaWdpQ2VydFRMU0h5YnJpZEVDQ1NIQTM4NDIwMjBDQTEtMS5j\ncmwwPgYDVR0gBDcwNTAzBgZngQwBAgIwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3\ndy5kaWdpY2VydC5jb20vQ1BTMIGFBggrBgEFBQcBAQR5MHcwJAYIKwYBBQUHMAGG\nGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBPBggrBgEFBQcwAoZDaHR0cDovL2Nh\nY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VExTSHlicmlkRUNDU0hBMzg0MjAy\nMENBMS0xLmNydDAJBgNVHRMEAjAAMIIBfwYKKwYBBAHWeQIEAgSCAW8EggFrAWkA\ndgCt9776fP8QyIudPZwePhhqtGcpXc+xDCTKhYY069yCigAAAX+Oi8SRAAAEAwBH\nMEUCIAR9cNnvYkZeKs9JElpeXwztYB2yLhtc8bB0rY2ke98nAiEAjiML8HZ7aeVE\nP/DkUltwIS4c73VVrG9JguoRrII7gWMAdwA1zxkbv7FsV78PrUxtQsu7ticgJlHq\nP+Eq76gDwzvWTAAAAX+Oi8R7AAAEAwBIMEYCIQDNckqvBhup7GpANMf0WPueytL8\nu/PBaIAObzNZeNMpOgIhAMjfEtE6AJ2fTjYCFh/BNVKk1mkTwBTavJlGmWomQyaB\nAHYAs3N3B+GEUPhjhtYFqdwRCUp5LbFnDAuH3PADDnk2pZoAAAF/jovErAAABAMA\nRzBFAiEA9Uj5Ed/XjQpj/MxQRQjzG0UFQLmgWlc73nnt3CJ7vskCICqHfBKlDz7R\nEHdV5Vk8bLMBW1Q6S7Ga2SbFuoVXs6zFMAoGCCqGSM49BAMDA2gAMGUCMCiVhqft\n7L/stBmv1XqSRNfE/jG/AqKIbmjGTocNbuQ7kt1Cs7kRg+b3b3C9Ipu5FQIxAM7c\ntGKrYDGt0pH8iF6rzbp9Q4HQXMZXkNxg+brjWxnaOVGTDNwNH7048+s/hT9bUQ==\n-----END CERTIFICATE-----", + ), + ExpectError: regexp.MustCompile(fmt.Sprintf("https certificate for '%s' already exist.", host)), + }, + }, + }) +} + +func testAccArgoCDRepositoryCertificates_Empty() string { + return ` +resource "argocd_repository_certificate" "simple" { +} +` +} + +func testAccArgoCDRepositoryCertificatesSSH(serverName, cert_subtype, cert_data string) string { + return fmt.Sprintf(` +resource "argocd_repository_certificate" "simple" { + ssh { + server_name = "%s" + cert_subtype = "%s" + cert_data = <[^\\s]+) (?P.+)$") + matches := re.FindAllStringSubmatch(string(stdout), 3) + + subTypesKeys := make([]string, 0) + for _, match := range matches { + subTypesKeys = append(subTypesKeys, match[1]) + subTypesKeys = append(subTypesKeys, match[2]) + } + + return nil, subTypesKeys +} diff --git a/argocd/schema_repository_certificates.go b/argocd/schema_repository_certificates.go new file mode 100644 index 00000000..6ec5958a --- /dev/null +++ b/argocd/schema_repository_certificates.go @@ -0,0 +1,81 @@ +package argocd + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func certificatesSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "ssh": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MaxItems: 1, + Description: "Defines a ssh certificate.", + ConflictsWith: []string{"https"}, + AtLeastOneOf: []string{"https", "ssh"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "server_name": { + Type: schema.TypeString, + Description: "ServerName specifies the DNS name of the server this certificate is intended for", + Required: true, + ForceNew: true, + }, + "cert_subtype": { + Type: schema.TypeString, + Description: "SubType specifies the sub type of the cert, i.e. `ssh-rsa`", + Required: true, + ForceNew: true, + }, + "cert_data": { + Type: schema.TypeString, + Description: "CertData contains the actual certificate data, dependent on the certificate type", + Required: true, + ForceNew: true, + }, + "cert_info": { + Type: schema.TypeString, + Description: "CertInfo will hold additional certificate info, dependent on the certificate type (e.g. SSH fingerprint, X509 CommonName)", + Computed: true, + }, + }, + }, + }, + "https": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MaxItems: 1, + ConflictsWith: []string{"ssh"}, + AtLeastOneOf: []string{"https", "ssh"}, + Description: "Defines a https certificate.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "server_name": { + Type: schema.TypeString, + Description: "ServerName specifies the DNS name of the server this certificate is intended for", + Required: true, + ForceNew: true, + }, + "cert_data": { + Type: schema.TypeString, + Description: "CertData contains the actual certificate data, dependent on the certificate type", + Required: true, + ForceNew: true, + }, + "cert_subtype": { + Type: schema.TypeString, + Description: "SubType specifies the sub type of the cert, i.e. `ssh-rsa`", + Computed: true, + }, + "cert_info": { + Type: schema.TypeString, + Description: "CertInfo will hold additional certificate info, dependent on the certificate type (e.g. SSH fingerprint, X509 CommonName)", + Computed: true, + }, + }, + }, + }, + } +} diff --git a/argocd/structure_repository_certificate.go b/argocd/structure_repository_certificate.go new file mode 100644 index 00000000..92244dd6 --- /dev/null +++ b/argocd/structure_repository_certificate.go @@ -0,0 +1,87 @@ +package argocd + +import ( + "context" + + application "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// Expand + +func expandRepositoryCertificate(d *schema.ResourceData) (*application.RepositoryCertificate, error) { + certificate := &application.RepositoryCertificate{} + + if _, ok := d.GetOk("ssh"); ok { + certificate.CertType = "ssh" + if v, ok := d.GetOk("ssh.0.server_name"); ok { + certificate.ServerName = v.(string) + } + if v, ok := d.GetOk("ssh.0.cert_subtype"); ok { + certificate.CertSubType = v.(string) + } + if v, ok := d.GetOk("ssh.0.cert_data"); ok { + certificate.CertData = []byte(v.(string)) + } + } else if _, ok := d.GetOk("https"); ok { + certificate.CertType = "https" + if v, ok := d.GetOk("https.0.server_name"); ok { + certificate.ServerName = v.(string) + } + if v, ok := d.GetOk("https.0.cert_data"); ok { + certificate.CertData = []byte(v.(string)) + } + } + return certificate, nil +} + +// Flatten + +func flattenRepositoryCertificate(certificate *application.RepositoryCertificate, d *schema.ResourceData, ctx context.Context) error { + var r map[string]interface{} + + if certificate.CertType == "ssh" { + r = map[string]interface{}{ + "ssh": []map[string]string{ + { + "server_name": certificate.ServerName, + "cert_subtype": certificate.CertSubType, + "cert_info": certificate.CertInfo, + "cert_data": getCertDataFromContextOrState(ctx, d, "ssh.0.cert_data"), + }, + }, + } + } else if certificate.CertType == "https" { + r = map[string]interface{}{ + "https": []map[string]string{ + { + "server_name": certificate.ServerName, + "cert_subtype": certificate.CertSubType, + "cert_info": certificate.CertInfo, + "cert_data": getCertDataFromContextOrState(ctx, d, "https.0.cert_data"), + }, + }, + } + } + + for k, v := range r { + if err := persistToState(k, v, d); err != nil { + return err + } + } + return nil +} + +// Since ArgoCD API does not return sensitive data, to avoid data drift : +// get cert_data from context if it has just been created +// or from current state if we are in a subsequent "Read" +func getCertDataFromContextOrState(ctx context.Context, d *schema.ResourceData, statePath string) (certData string) { + if v, ok := ctx.Value("cert_data").([]byte); ok { + certData = string(v) + } else { + certData = d.Get(statePath).(string) + } + + return +} diff --git a/argocd/validators.go b/argocd/validators.go index 35266b3a..3c28eb58 100644 --- a/argocd/validators.go +++ b/argocd/validators.go @@ -2,13 +2,14 @@ package argocd import ( "fmt" + "regexp" + "strings" + "github.com/argoproj/pkg/time" "github.com/robfig/cron" "golang.org/x/crypto/ssh" apiValidation "k8s.io/apimachinery/pkg/api/validation" utilValidation "k8s.io/apimachinery/pkg/util/validation" - "regexp" - "strings" ) func validateMetadataLabels(value interface{}, key string) (ws []string, es []error) { diff --git a/docs/resources/repository_certificate.md b/docs/resources/repository_certificate.md new file mode 100644 index 00000000..9d5c2d5e --- /dev/null +++ b/docs/resources/repository_certificate.md @@ -0,0 +1,64 @@ +# argocd_repository_certificate + +Creates an ArgoCD certificate, for use with future or existing private repositories. + +## Example Usage + +### Example ssh certificate +```hcl +// Private repository ssh certificate +resource "argocd_repository_certificate" "private-git-repository" { + ssh { + server_name = "private-git-repository.local" + cert_subtype = "ssh-rsa" + cert_data = <