-
Notifications
You must be signed in to change notification settings - Fork 42
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adding jenkins_credential_username resource (#15)
* Adding jenkins_credential_username resource * Exposing credential ID to user * Merge coverage reports * Test updates * Documentation * Don't need the "2"s anymore * Add warning about folder templates * Validate folder name exists
- Loading branch information
Showing
6 changed files
with
358 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# jenkins_credential_username Resource | ||
|
||
Manages a username credential within Jenkins. This username may then be referenced within jobs that are created. | ||
|
||
~> The "password" property may leave plain-text passwords in your state file. If using the property to manage the password 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. | ||
|
||
## Example Usage | ||
|
||
```hcl | ||
resource jenkins_credential_username example { | ||
name = "example-username" | ||
username = "example" | ||
password = "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. | ||
* `username` - (Required) The username to be associated with the credentials. | ||
* `password` - (Optional) The password to be associated with the credentials. If empty then the password property will become unmanaged and expected to be set manually within Jenkins. If set then the password will be updated only upon changes -- if the password is set manually within Jenkins then it will not reconcile this drift until the next time the password property is changed. | ||
|
||
## Attribute Reference | ||
|
||
All arguments above are exported. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
resource jenkins_credential_username global { | ||
name = "global-username" | ||
username = "foo" | ||
# Passwords may be unmanaged | ||
# password = "barsoom" | ||
} | ||
|
||
resource jenkins_credential_username folder { | ||
name = "folder-username" | ||
folder = jenkins_folder.example.name | ||
username = "folder-foo" | ||
password = "barsoom" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
package jenkins | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"strings" | ||
|
||
jenkins "github.com/bndr/gojenkins" | ||
"github.com/hashicorp/go-cty/cty" | ||
"github.com/hashicorp/terraform-plugin-sdk/v2/diag" | ||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" | ||
) | ||
|
||
var supportedCredentialScopes = []string{"SYSTEM", "GLOBAL"} | ||
|
||
func resourceJenkinsCredentialUsername() *schema.Resource { | ||
return &schema.Resource{ | ||
CreateContext: resourceJenkinsCredentialUsernameCreate, | ||
ReadContext: resourceJenkinsCredentialUsernameRead, | ||
UpdateContext: resourceJenkinsCredentialUsernameUpdate, | ||
DeleteContext: resourceJenkinsCredentialUsernameDelete, | ||
Importer: &schema.ResourceImporter{ | ||
StateContext: resourceJenkinsCredentialUsernameImport, | ||
}, | ||
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", | ||
}, | ||
"username": { | ||
Type: schema.TypeString, | ||
Description: "The credentials user username.", | ||
Required: true, | ||
}, | ||
"password": { | ||
Type: schema.TypeString, | ||
Description: "The credentials user password. If left empty will be unmanaged.", | ||
Optional: true, | ||
Sensitive: true, | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
func resourceJenkinsCredentialUsernameCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { | ||
client := meta.(jenkinsClient) | ||
cm := client.Credentials() | ||
|
||
// Validate that the folder exists | ||
cm.Folder = d.Get("folder").(string) | ||
if cm.Folder != "" { | ||
if _, err := client.GetJob(formatJobName(cm.Folder)); err != nil { | ||
return diag.Errorf("Invalid folder name '%s' specified: %s", cm.Folder, err) | ||
} | ||
} | ||
|
||
cred := jenkins.UsernameCredentials{ | ||
ID: d.Get("name").(string), | ||
Scope: d.Get("scope").(string), | ||
Description: d.Get("description").(string), | ||
Username: d.Get("username").(string), | ||
Password: d.Get("password").(string), | ||
} | ||
|
||
domain := d.Get("domain").(string) | ||
err := cm.Add(domain, cred) | ||
if err != nil { | ||
return diag.Errorf("Could not create username credentials: %s", err) | ||
} | ||
|
||
d.SetId(generateCredentialID(cm.Folder, cred.ID)) | ||
return resourceJenkinsCredentialUsernameRead(ctx, d, meta) | ||
} | ||
|
||
func resourceJenkinsCredentialUsernameRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { | ||
cm := meta.(jenkinsClient).Credentials() | ||
cm.Folder = d.Get("folder").(string) | ||
|
||
cred := jenkins.UsernameCredentials{} | ||
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 username credentials: %s", err) | ||
} | ||
|
||
d.SetId(generateCredentialID(cm.Folder, cred.ID)) | ||
d.Set("scope", cred.Scope) | ||
d.Set("description", cred.Description) | ||
d.Set("username", cred.Username) | ||
// 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 resourceJenkinsCredentialUsernameUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { | ||
cm := meta.(jenkinsClient).Credentials() | ||
cm.Folder = d.Get("folder").(string) | ||
|
||
domain := d.Get("domain").(string) | ||
cred := jenkins.UsernameCredentials{ | ||
ID: d.Get("name").(string), | ||
Scope: d.Get("scope").(string), | ||
Description: d.Get("description").(string), | ||
Username: d.Get("username").(string), | ||
} | ||
|
||
// Only enforce the password if it is non-empty | ||
if d.Get("password").(string) != "" { | ||
cred.Password = d.Get("password").(string) | ||
} | ||
|
||
err := cm.Update(domain, d.Get("name").(string), &cred) | ||
if err != nil { | ||
return diag.Errorf("Could not update username credentials: %s", err) | ||
} | ||
|
||
d.SetId(generateCredentialID(cm.Folder, cred.ID)) | ||
return resourceJenkinsCredentialUsernameRead(ctx, d, meta) | ||
} | ||
|
||
func resourceJenkinsCredentialUsernameDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { | ||
cm := meta.(jenkinsClient).Credentials() | ||
cm.Folder = 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 resourceJenkinsCredentialUsernameImport(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 | ||
} | ||
|
||
func validateCredentialScope(v interface{}, p cty.Path) diag.Diagnostics { | ||
for _, supported := range supportedCredentialScopes { | ||
if v == supported { | ||
return nil | ||
} | ||
} | ||
return diag.Errorf("Invalid scope: %s. Supported scopes are: %s", v, strings.Join(supportedCredentialScopes, ", ")) | ||
} | ||
|
||
func generateCredentialID(folder, name string) string { | ||
return fmt.Sprintf("%s/%s", folder, name) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
package jenkins | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
|
||
jenkins "github.com/bndr/gojenkins" | ||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" | ||
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform" | ||
) | ||
|
||
func TestAccJenkinsCredentialUsername_basic(t *testing.T) { | ||
var cred jenkins.UsernameCredentials | ||
// randString := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) | ||
|
||
resource.Test(t, resource.TestCase{ | ||
PreCheck: func() { testAccPreCheck(t) }, | ||
Providers: testAccProviders, | ||
CheckDestroy: testAccCheckJenkinsCredentialUsernameDestroy, | ||
Steps: []resource.TestStep{ | ||
{ | ||
Config: fmt.Sprintf(` | ||
resource jenkins_credential_username foo { | ||
name = "test-username" | ||
username = "foo" | ||
password = "bar" | ||
}`), | ||
Check: resource.ComposeTestCheckFunc( | ||
resource.TestCheckResourceAttr("jenkins_credential_username.foo", "id", "/test-username"), | ||
testAccCheckJenkinsCredentialUsernameExists("jenkins_credential_username.foo", &cred), | ||
), | ||
}, | ||
{ | ||
// Update by adding description | ||
Config: fmt.Sprintf(` | ||
resource jenkins_credential_username foo { | ||
name = "test-username" | ||
description = "new-description" | ||
username = "foo" | ||
password = "bar" | ||
}`), | ||
Check: resource.ComposeTestCheckFunc( | ||
resource.TestCheckResourceAttr("jenkins_credential_username.foo", "description", "new-description"), | ||
testAccCheckJenkinsCredentialUsernameExists("jenkins_credential_username.foo", &cred), | ||
testAccCheckJenkinsCredentialUsernameDescriptionUpdated(&cred, "new-description"), | ||
), | ||
}, | ||
}, | ||
}) | ||
} | ||
|
||
func testAccCheckJenkinsCredentialUsernameExists(resourceName string, cred *jenkins.UsernameCredentials) resource.TestCheckFunc { | ||
return func(s *terraform.State) error { | ||
client := testAccProvider.Meta().(jenkinsClient) | ||
|
||
rs, ok := s.RootModule().Resources[resourceName] | ||
if !ok { | ||
return fmt.Errorf(resourceName + " not found") | ||
} | ||
|
||
if rs.Primary.ID == "" { | ||
return fmt.Errorf("ID is not set") | ||
} | ||
|
||
err := client.Credentials().GetSingle(rs.Primary.Attributes["domain"], rs.Primary.Attributes["name"], cred) | ||
if err != nil { | ||
return fmt.Errorf("Unable to retrieve credentials for %s: %w", rs.Primary.ID, err) | ||
} | ||
|
||
return nil | ||
} | ||
} | ||
|
||
func testAccCheckJenkinsCredentialUsernameDescriptionUpdated(cred *jenkins.UsernameCredentials, description string) resource.TestCheckFunc { | ||
return func(s *terraform.State) error { | ||
if cred.Description != description { | ||
return fmt.Errorf("Description was not set") | ||
} | ||
|
||
return nil | ||
} | ||
} | ||
|
||
func testAccCheckJenkinsCredentialUsernameDestroy(s *terraform.State) error { | ||
client := testAccProvider.Meta().(jenkinsClient) | ||
|
||
for _, rs := range s.RootModule().Resources { | ||
if rs.Type != "jenkins_credential_username" { | ||
continue | ||
} | ||
|
||
cred := jenkins.UsernameCredentials{} | ||
err := client.Credentials().GetSingle(rs.Primary.Meta["domain"].(string), rs.Primary.Meta["name"].(string), &cred) | ||
if err == nil { | ||
return fmt.Errorf("Credentials %s still exists", rs.Primary.ID) | ||
} | ||
} | ||
|
||
return nil | ||
} |