Skip to content

Commit

Permalink
jenkins_credential_vault_approle (taiidani#25)
Browse files Browse the repository at this point in the history
* Add jenkins_credential_vault_approle

* Update docs/resources/credential_vault_approle.md

Co-authored-by: Ryan Nixon <r.taiidani@gmail.com>
  • Loading branch information
mishak87 and taiidani committed Jan 2, 2021
1 parent d7a122d commit 83b64c6
Show file tree
Hide file tree
Showing 5 changed files with 426 additions and 4 deletions.
35 changes: 35 additions & 0 deletions docs/resources/credential_vault_approle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# jenkins_credential_vault_approle Resource

Manages a Vault AppRole credential within Jenkins. This credential may then be referenced within jobs that are created.

~> The "secret_id" property may leave plain-text secret id in your state file. If using the property to manage the secret id in Terraform, ensure that your state file is properly secured and encrypted at rest.

~> When using this resource within a folder context it can conflict with the [folder resource](folder) template. When using these in combination you may need to add a lifecycle `ignore_changes` rule to the folder's `template` property.
~> The Jenkins installation that uses this resource is expected to have the [Hashicorp Vault Plugin](https://plugins.jenkins.io/hashicorp-vault-plugin/) installed in their system.

## Example Usage

```hcl
resource jenkins_credential_vault_approle example {
name = "example-approle"
role_id = "example"
secret_id = "super-secret"
}
```

## Argument Reference

The following arguments are supported:

* `name` - (Required) The name of the credentials being created. This maps to the ID property within Jenkins, and cannot be changed once set.
* `domain` - (Optional) The domain store to place the credentials into. If not set will default to the global credentials store.
* `folder` - (Optional) The folder namespace to store the credentials in. If not set will default to global Jenkins credentials.
* `scope` - (Optional) The visibility of the credentials to Jenkins agents. This must be set to either "GLOBAL" or "SYSTEM". If not set will default to "GLOBAL".
* `description` - (Optional) A human readable description of the credentials being stored.
* `path` - (Optional) The unique name of the approle auth backend. Defaults to `approle`.
* `role_id` - (Required) The role_id to be associated with the credentials.
* `secret_id` - (Optional) The secret_id to be associated with the credentials. If empty then the secret_id property will become unmanaged and expected to be set manually within Jenkins. If set then the secret_id will be updated only upon changes -- if the secret_id is set manually within Jenkins then it will not reconcile this drift until the next time the secret_id property is changed.

## Attribute Reference

All arguments above are exported.
2 changes: 1 addition & 1 deletion example/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
FROM jenkins/jenkins:lts

RUN /usr/local/bin/install-plugins.sh cloudbees-folder pipeline-model-definition git
RUN /usr/local/bin/install-plugins.sh hashicorp-vault-plugin cloudbees-folder pipeline-model-definition git

HEALTHCHECK --interval=4s --start-period=5s --retries=30 CMD [ "curl", "-f", "http://localhost:8080" ]
7 changes: 4 additions & 3 deletions jenkins/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ func Provider() *schema.Provider {
},

ResourcesMap: map[string]*schema.Resource{
"jenkins_credential_username": resourceJenkinsCredentialUsername(),
"jenkins_folder": resourceJenkinsFolder(),
"jenkins_job": resourceJenkinsJob(),
"jenkins_credential_username": resourceJenkinsCredentialUsername(),
"jenkins_credential_vault_approle": resourceJenkinsCredentialVaultAppRole(),
"jenkins_folder": resourceJenkinsFolder(),
"jenkins_job": resourceJenkinsJob(),
},

ConfigureContextFunc: configureProvider,
Expand Down
210 changes: 210 additions & 0 deletions jenkins/resource_jenkins_credential_vault_approle.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package jenkins

import (
"context"
"encoding/xml"
"fmt"
"strings"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

// VaultAppRoleCredentials struct representing credential for storing Vault AppRole role id and secret id
type VaultAppRoleCredentials struct {
XMLName xml.Name `xml:"com.datapipe.jenkins.vault.credentials.VaultAppRoleCredential"`
ID string `xml:"id"`
Scope string `xml:"scope"`
Description string `xml:"description"`
Path string `xml:"path"`
RoleID string `xml:"roleId"`
SecretID string `xml:"secretId"`
}

func resourceJenkinsCredentialVaultAppRole() *schema.Resource {
return &schema.Resource{
CreateContext: resourceJenkinsCredentialVaultAppRoleCreate,
ReadContext: resourceJenkinsCredentialVaultAppRoleRead,
UpdateContext: resourceJenkinsCredentialVaultAppRoleUpdate,
DeleteContext: resourceJenkinsCredentialVaultAppRoleDelete,
Importer: &schema.ResourceImporter{
StateContext: resourceJenkinsCredentialVaultAppRoleImport,
},
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Description: "The identifier assigned to the credentials.",
Required: true,
ForceNew: true,
},
"domain": {
Type: schema.TypeString,
Description: "The domain namespace that the credentials will be added to.",
Optional: true,
Default: "_",
// In-place updates should be possible, but gojenkins does not support move operations
ForceNew: true,
},
"folder": {
Type: schema.TypeString,
Description: "The folder namespace that the credentials will be added to.",
Optional: true,
ForceNew: true,
},
"scope": {
Type: schema.TypeString,
Description: "The Jenkins scope assigned to the credentials.",
Optional: true,
Default: "GLOBAL",
ValidateDiagFunc: validateCredentialScope,
},
"description": {
Type: schema.TypeString,
Description: "The credentials descriptive text.",
Optional: true,
Default: "Managed by Terraform",
},
"path": {
Type: schema.TypeString,
Description: "Path of the roles approle backend.",
Optional: true,
Default: "approle",
},
"role_id": {
Type: schema.TypeString,
Description: "The roles role_id.",
Required: true,
},
"secret_id": {
Type: schema.TypeString,
Description: "The roles secret_id. If left empty will be unmanaged.",
Optional: true,
Sensitive: true,
},
},
}
}

func resourceJenkinsCredentialVaultAppRoleCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(jenkinsClient)
cm := client.Credentials()
cm.Folder = formatFolderName(d.Get("folder").(string))
// return diag.FromErr(fmt.Errorf("invalid folder name '%s', '%s'", cm.Folder, d.Get("folder").(string)))
// Validate that the folder exists
if err := folderExists(client, cm.Folder); err != nil {
return diag.FromErr(fmt.Errorf("invalid folder name '%s' specified: %w", cm.Folder, err))
}

cred := VaultAppRoleCredentials{
ID: d.Get("name").(string),
Scope: d.Get("scope").(string),
Description: d.Get("description").(string),
Path: d.Get("path").(string),
RoleID: d.Get("role_id").(string),
SecretID: d.Get("secret_id").(string),
}

domain := d.Get("domain").(string)
err := cm.Add(domain, cred)
if err != nil {
return diag.Errorf("Could not create vault approle credentials: %s", err)
}

d.SetId(generateCredentialID(d.Get("folder").(string), cred.ID))
return resourceJenkinsCredentialVaultAppRoleRead(ctx, d, meta)
}

func resourceJenkinsCredentialVaultAppRoleRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
cm := meta.(jenkinsClient).Credentials()
cm.Folder = formatFolderName(d.Get("folder").(string))

cred := VaultAppRoleCredentials{}
err := cm.GetSingle(
d.Get("domain").(string),
d.Get("name").(string),
&cred,
)

if err != nil {
if strings.HasSuffix(err.Error(), "404") {
// Job does not exist
d.SetId("")
return nil
}

return diag.Errorf("Could not read vault approle credentials: %s", err)
}

d.SetId(generateCredentialID(d.Get("folder").(string), cred.ID))
d.Set("scope", cred.Scope)
d.Set("description", cred.Description)
d.Set("path", cred.Path)
d.Set("role_id", cred.RoleID)
// NOTE: We are NOT setting the password here, as the password returned by GetSingle is garbage
// Password only applies to Create/Update operations if the "password" property is non-empty

return nil
}

func resourceJenkinsCredentialVaultAppRoleUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
cm := meta.(jenkinsClient).Credentials()
cm.Folder = formatFolderName(d.Get("folder").(string))

domain := d.Get("domain").(string)
cred := VaultAppRoleCredentials{
ID: d.Get("name").(string),
Scope: d.Get("scope").(string),
Description: d.Get("description").(string),
Path: d.Get("path").(string),
RoleID: d.Get("role_id").(string),
}

// Only enforce the password if it is non-empty
if d.Get("secret_id").(string) != "" {
cred.SecretID = d.Get("secret_id").(string)
}

err := cm.Update(domain, d.Get("name").(string), &cred)
if err != nil {
return diag.Errorf("Could not update vault approle credentials: %s", err)
}

d.SetId(generateCredentialID(d.Get("folder").(string), cred.ID))
return resourceJenkinsCredentialVaultAppRoleRead(ctx, d, meta)
}

func resourceJenkinsCredentialVaultAppRoleDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
cm := meta.(jenkinsClient).Credentials()
cm.Folder = formatFolderName(d.Get("folder").(string))

err := cm.Delete(
d.Get("domain").(string),
d.Get("name").(string),
)
if err != nil {
return diag.FromErr(err)
}

return nil
}

func resourceJenkinsCredentialVaultAppRoleImport(ctx context.Context, d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) {
ret := []*schema.ResourceData{d}

splitID := strings.Split(d.Id(), "/")
if len(splitID) < 2 {
return ret, fmt.Errorf("import ID was improperly formatted. Imports need to be in the format \"[<folder>/]<domain>/<name>\"")
}

name := splitID[len(splitID)-1]
d.Set("name", name)

domain := splitID[len(splitID)-2]
d.Set("domain", domain)

folder := strings.Trim(strings.Join(splitID[0:len(splitID)-2], "/"), "/")
d.Set("folder", folder)

d.SetId(generateCredentialID(folder, name))
return ret, nil
}
Loading

0 comments on commit 83b64c6

Please sign in to comment.