diff --git a/.gitignore b/.gitignore index 983d7da..6dc13be 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,5 @@ -drp vendor bin -.terraform -._Version.meta -terraform-provider-drp -terraform-provider-drp.sha256 terraform-provider-drp.zip -terraform.sha256 -terraform.yaml -terraform.json +terraform-provider-drp.sha256 +.terraform diff --git a/.travis.yml b/.travis.yml index 6c9de46..69f0db5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,11 +11,15 @@ go: git: depth: 500 install: +- go get github.com/kardianos/govendor - ./tools/build.sh - ./tools/package.sh - ./tools/publish.sh script: -- ./tools/test.sh +- make test +- make testacc +- make vendor-status +- make vet after_success: - bash <(curl -s https://codecov.io/bash) -t 6259c706-5c1d-471f-b62b-4f908ea1801d notifications: @@ -29,8 +33,6 @@ deploy: file: - terraform-provider-drp.zip - terraform-provider-drp.sha256 - - terraform.yaml - - terraform.sha256 skip_cleanup: true on: repo: rackn/terraform-provider-drp diff --git a/GNUmakefile b/GNUmakefile new file mode 100644 index 0000000..c9eacb4 --- /dev/null +++ b/GNUmakefile @@ -0,0 +1,47 @@ +TEST?=$$(go list ./... |grep -v 'vendor') +GOFMT_FILES?=$$(find . -name '*.go' |grep -v vendor) + +default: build + +build: fmtcheck + go install + +test: fmtcheck + go test -i $(TEST) || exit 1 + echo $(TEST) | \ + xargs -t -n4 go test $(TESTARGS) -timeout=30s -parallel=4 + +testacc: fmtcheck + TF_ACC=1 go test $(TEST) -v $(TESTARGS) -timeout 120m + +vet: + @echo "go vet ." + @go vet $$(go list ./... | grep -v vendor/) ; if [ $$? -eq 1 ]; then \ + echo ""; \ + echo "Vet found suspicious constructs. Please check the reported constructs"; \ + echo "and fix them if necessary before submitting the code for review."; \ + exit 1; \ + fi + +fmt: + gofmt -w $(GOFMT_FILES) + +fmtcheck: + @sh -c "'$(CURDIR)/scripts/gofmtcheck.sh'" + +errcheck: + @sh -c "'$(CURDIR)/scripts/errcheck.sh'" + +vendor-status: + @govendor status + +test-compile: + @if [ "$(TEST)" = "./..." ]; then \ + echo "ERROR: Set TEST to a specific package. For example,"; \ + echo " make test-compile TEST=./aws"; \ + exit 1; \ + fi + go test -c $(TEST) $(TESTARGS) + +.PHONY: build test testacc vet fmt fmtcheck errcheck vendor-status test-compile + diff --git a/README.md b/README.md index e319f24..b7bff79 100644 --- a/README.md +++ b/README.md @@ -1 +1,64 @@ -# terraform-provider-drp +Terraform Provider +================== + +- Website: https://www.terraform.io +- [![Gitter chat](https://badges.gitter.im/hashicorp-terraform/Lobby.png)](https://gitter.im/hashicorp-terraform/Lobby) +- Mailing list: [Google Groups](http://groups.google.com/group/terraform-tool) + + + +Requirements +------------ + +- [Terraform](https://www.terraform.io/downloads.html) 0.10.x +- [Go](https://golang.org/doc/install) 1.8 (to build the provider plugin) + +Building The Provider +--------------------- + +Clone repository to: `$GOPATH/src/github.com/hashicorp/terraform-provider-drp` + +```sh +$ mkdir -p $GOPATH/src/github.com/hashicorp; cd $GOPATH/src/github.com/hashicorp +$ git clone git@github.com:hashicorp/terraform-provider-drp +``` + +Enter the provider directory and build the provider + +```sh +$ cd $GOPATH/src/github.com/hashicorp/terraform-provider-drp +$ make build +``` + +Using the provider +---------------------- +## Fill in for each provider + +Developing the Provider +--------------------------- + +If you wish to work on the provider, you'll first need [Go](http://www.golang.org) installed on your machine (version 1.9+ is *required*). You'll also need to correctly setup a [GOPATH](http://golang.org/doc/code.html#GOPATH), as well as adding `$GOPATH/bin` to your `$PATH`. + +To compile the provider, run `make build`. This will build the provider and put the provider binary in the `$GOPATH/bin` directory. + +```sh +$ make bin +... +$ $GOPATH/bin/terraform-provider-drp +... +``` + +In order to test the provider, you can simply run `make test`. + +```sh +$ make test +``` + +In order to run the full suite of Acceptance tests, run `make testacc`. + +*Note:* Acceptance tests create real resources, and often cost money to run. +*Note:* In this case, acceptances run locally without external resources. + +```sh +$ make testacc +``` diff --git a/client/client.go b/client/client.go deleted file mode 100644 index 1593030..0000000 --- a/client/client.go +++ /dev/null @@ -1,459 +0,0 @@ -package client - -import ( - "bytes" - "crypto/tls" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "log" - "net" - "net/http" - "net/url" - "strings" - "time" - - "github.com/VictorLowther/jsonpatch2" - "github.com/VictorLowther/jsonpatch2/utils" - "github.com/digitalrebar/provision/models" - "github.com/hashicorp/terraform/helper/resource" -) - -type Client struct { - APIKey string - APIUser string - APIPassword string - APIURL string - - netClient *http.Client -} - -/* - * Builds a client object for this config - */ -func (c *Client) Client() (interface{}, error) { - log.Println("[DEBUG] [Config.Client] Configuring the DRP API client") - - var netTransport = &http.Transport{ - Dial: (&net.Dialer{ - Timeout: 5 * time.Second, - }).Dial, - TLSHandshakeTimeout: 5 * time.Second, - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - var netClient = &http.Client{ - Timeout: time.Second * 10, - Transport: netTransport, - } - c.netClient = netClient - - return c, nil -} - -func (c *Client) buildRequest(method, path string, data io.Reader) (*http.Request, error) { - request, err := http.NewRequest(method, c.APIURL+"/api/v3/"+path, data) - if err != nil { - log.Printf("[DEBUG] [buildRequest] %s request error = %v\n", method, err) - return nil, err - } - - if c.APIKey != "" { - request.Header.Set("Authorization", "Bearer "+c.APIKey) - } else { - hdr := base64.StdEncoding.EncodeToString([]byte(c.APIUser + ":" + c.APIPassword)) - request.Header.Set("Authorization", "Basic "+hdr) - } - return request, nil -} - -func (c *Client) getToken(machineId string) (string, error) { - request, err := c.buildRequest("GET", "users/"+c.APIUser+"/token", nil) - if err != nil { - return "", err - } - - q := request.URL.Query() - q.Add("ttl", "3600") - q.Add("scope", "machines") - q.Add("specific", machineId) - - request.URL.RawQuery = q.Encode() - - if response, err := c.netClient.Do(request); err != nil { - log.Printf("[DEBUG] [getToken] call error = %v\n", err) - return "", err - } else { - defer response.Body.Close() - - // We aren't authorized - if response.StatusCode == http.StatusUnauthorized || response.StatusCode == http.StatusForbidden { - return "", fmt.Errorf("getToken: Unauthorized access") - } - - // We got an error - if response.StatusCode > 299 || response.StatusCode < 200 { - berr := models.Error{} - if err := json.NewDecoder(response.Body).Decode(&berr); err != nil { - return "", err - } else { - return "", &berr - } - } - - // Gots data - var data models.UserToken - err := json.NewDecoder(response.Body).Decode(&data) - if err != nil { - return "", fmt.Errorf("getToken: unmarshall error: %v", err) - } - return data.Token, nil - } -} - -func (c *Client) doGet(path string, params url.Values, data interface{}) error { - request, err := c.buildRequest("GET", path, nil) - if err != nil { - return err - } - - q := request.URL.Query() - q.Add("terraform/managed", "true") - q.Add("terraform/allocated", "false") - for _, s := range params["filters"] { - arr := strings.SplitN(s, "=", 2) - q.Add(arr[0], arr[1]) - } - request.URL.RawQuery = q.Encode() - - if response, err := c.netClient.Do(request); err != nil { - log.Printf("[DEBUG] [doGet] call error = %v\n", err) - return err - } else { - defer response.Body.Close() - - // We aren't authorized - if response.StatusCode == http.StatusUnauthorized || response.StatusCode == http.StatusForbidden { - return fmt.Errorf("Unauthorized access") - } - - // We got an error - if response.StatusCode > 299 || response.StatusCode < 200 { - berr := models.Error{} - if err := json.NewDecoder(response.Body).Decode(&berr); err != nil { - return err - } else { - return &berr - } - } - - // Gots data - return json.NewDecoder(response.Body).Decode(data) - } -} -func (c *Client) doPatch(path string, patch jsonpatch2.Patch, data interface{}) error { - jp, err := json.Marshal(patch) - if err != nil { - return fmt.Errorf("Failed to marshal patch: %v", err) - } - - request, err := c.buildRequest("PATCH", path, bytes.NewBuffer(jp)) - if err != nil { - log.Printf("[DEBUG] [doPatch] failed to build requiest error = %v\n", err) - return err - } - - q := request.URL.Query() - q.Add("force", "true") - request.URL.RawQuery = q.Encode() - request.Header.Set("Content-Type", "application/json") - - if response, err := c.netClient.Do(request); err != nil { - log.Printf("[DEBUG] [doPatch] call error = %v\n", err) - return err - } else { - defer response.Body.Close() - - // We aren't authorized - if response.StatusCode == http.StatusUnauthorized || response.StatusCode == http.StatusForbidden { - log.Printf("[DEBUG] [doPatch] unauthorized\n") - return fmt.Errorf("Unauthorized access") - } - - // We got an error - if response.StatusCode > 299 || response.StatusCode < 200 { - berr := models.Error{} - if err := json.NewDecoder(response.Body).Decode(&berr); err != nil { - log.Printf("[DEBUG] [doPatch] responded error = %v\n", err) - return err - } else { - log.Printf("[DEBUG] [doPatch] berr: responded error = %v\n", berr) - return &berr - } - } - - // Gots data - return json.NewDecoder(response.Body).Decode(data) - } -} - -// Gets all managed and unallocated machines (in addition to the other params) -func (c *Client) getAllMachines(params url.Values) ([]*models.Machine, error) { - log.Printf("[DEBUG] [getAllMachines] Getting all machines from DRP\n") - data := []*models.Machine{} - return data, c.doGet("machines", params, &data) -} - -func (c *Client) getSingleMachine(uuid string) (*models.Machine, error) { - log.Printf("[DEBUG] [getSingleMachine] Getting a machine (%s) from DRP\n", uuid) - data := &models.Machine{} - return data, c.doGet("machines/"+uuid, map[string][]string{}, data) -} - -func (c *Client) AllocateMachine(params url.Values) (*models.Machine, error) { - log.Printf("[DEBUG] [allocateMachines] Allocating a machine with following params: %+v", params) - for { - if machines, err := c.getAllMachines(params); err != nil { - return nil, err - } else { - if len(machines) == 0 { - return nil, fmt.Errorf("No machines available") - } - machine := machines[0] - - var baseObj []byte - var merged []byte - var err error - if baseObj, err = json.Marshal(machine); err != nil { - return nil, fmt.Errorf("Error marshalling baseObj: %v", err) - } - - if machine.Profile.Params == nil { - machine.Profile.Params = map[string]interface{}{} - } - machine.Profile.Params["terraform/allocated"] = true - - if merged, err = json.Marshal(machine); err != nil { - return nil, fmt.Errorf("Error marshalling merged: %v", err) - } - - patch := jsonpatch2.Patch{} - if pdata, err := jsonpatch2.Generate(baseObj, merged, true); err != nil { - return nil, fmt.Errorf("Error generating patch: %v", err) - } else if err := utils.Remarshal(&pdata, &patch); err != nil { - return nil, fmt.Errorf("Error translating patch: %v", err) - } - - retMachine := &models.Machine{} - err = c.doPatch("machines/"+machine.UUID(), patch, retMachine) - if err != nil { - berr, ok := err.(*models.Error) - if ok { - // If we get a patch error, the machine was allocated while we were - // waiting. Try again. - if berr.Type == "PATCH" && (berr.Code == 406 || berr.Code == 409) { - continue - } - } - return nil, err - } - - return retMachine, nil - } - } -} - -func (c *Client) ReleaseMachine(uuid string) error { - log.Printf("[DEBUG] [releaseMachine] Releasing machine: %s", uuid) - if machine, err := c.getSingleMachine(uuid); err != nil { - return err - } else { - for { - var baseObj []byte - var merged []byte - var err error - if baseObj, err = json.Marshal(machine); err != nil { - return fmt.Errorf("Error marshalling baseObj: %v", err) - } - - if machine.Profile.Params == nil { - machine.Profile.Params = map[string]interface{}{} - } - - // Force this back through discovery - machine.Profile.Params["terraform/allocated"] = false - machine.Profile.Params["terraform/managed"] = false - - if merged, err = json.Marshal(machine); err != nil { - return fmt.Errorf("Error marshalling merged: %v", err) - } - - patch := jsonpatch2.Patch{} - if pdata, err := jsonpatch2.Generate(baseObj, merged, true); err != nil { - return fmt.Errorf("Error generating patch: %v", err) - } else if err := utils.Remarshal(&pdata, &patch); err != nil { - return fmt.Errorf("Error translating patch: %v", err) - } - - retMachine := &models.Machine{} - err = c.doPatch("machines/"+machine.UUID(), patch, retMachine) - if err != nil { - berr, ok := err.(*models.Error) - if ok { - // If we get a patch error, the machine was allocated while we were - // waiting. Try again. - if berr.Type == "JsonPatchError" { - continue - } - } - return err - } - return nil - } - } -} - -// Update the machine to request position -func (c *Client) UpdateMachine(machineObj *models.Machine, constraints url.Values) error { - oj, err := json.Marshal(machineObj) - if err != nil { - return err - } - - // Apply the changes - if machineObj.Profile.Params == nil { - machineObj.Profile.Params = map[string]interface{}{} - } - if val, set := constraints["bootenv"]; set { - machineObj.BootEnv = val[0] - } - if val, set := constraints["stage"]; set { - machineObj.Stage = val[0] - } - if val, set := constraints["description"]; set { - machineObj.Description = val[0] - } - if val, set := constraints["name"]; set { - machineObj.Name = val[0] - } - if val, set := constraints["owner"]; set { - machineObj.Profile.Params["terraform/owner"] = val[0] - } - if val, set := constraints["userdata"]; set { - machineObj.Profile.Params["cloud-init/user-data"] = val[0] - } - if val, set := constraints["profiles"]; set { - for _, p := range val { - found := false - for _, pp := range machineObj.Profiles { - if pp == p { - found = true - break - } - } - if !found { - machineObj.Profiles = append(machineObj.Profiles, p) - } - } - } - if val, set := constraints["parameters"]; set { - for _, parm := range val { - arr := strings.SplitN(parm, "=", 2) - - // GREG: convert types from string to whatever - machineObj.Profile.Params[arr[0]] = arr[1] - } - } - - nj, err := json.Marshal(machineObj) - if err != nil { - return err - } - - patch, err := jsonpatch2.Generate(oj, nj, true) - if err != nil { - return fmt.Errorf("Error generating patch: %v", err) - } - - return c.doPatch("machines/"+machineObj.UUID(), patch, machineObj) -} - -func (c *Client) ExistsMachine(uuid string) (bool, error) { - log.Printf("[DEBUG] [ExistsMachine] Getting stat of machine: %s", uuid) - _, err := c.getSingleMachine(uuid) - return err == nil, err -} - -func (c *Client) GetMachine(uuid string) (*models.Machine, error) { - log.Printf("[DEBUG] [GetMachine] Getting machine: %s", uuid) - return c.getSingleMachine(uuid) -} - -func (c *Client) GetMachineStatus(uuid string) resource.StateRefreshFunc { - log.Printf("[DEBUG] [getMachineStatus] Getting status of machine: %s", uuid) - return func() (interface{}, string, error) { - machineObject, err := c.getSingleMachine(uuid) - if err != nil { - log.Printf("[ERROR] [getMachineStatus] Unable to get machine: %s\n", uuid) - return nil, "", err - } - - machineStatus := "6" - if machineObject.Stage != "" { - if machineObject.Stage != "complete" && machineObject.Stage != "complete-nowait" { - machineStatus = "9" - } - } else { - if machineObject.BootEnv != "local" { - machineStatus = "9" - } - } - - var statusRetVal bytes.Buffer - statusRetVal.WriteString(machineStatus) - statusRetVal.WriteString(":") - - return machineObject, statusRetVal.String(), nil - } -} - -func (c *Client) MachineDo(uuid, action string, params url.Values) error { - log.Printf("[DEBUG] [machineDo] uuid: %s, action: %s, params: %+v", uuid, action, params) - - td := map[string]interface{}{} - jp, err := json.Marshal(td) - if err != nil { - return fmt.Errorf("Failed to marshal empty map: %v", err) - } - request, err := c.buildRequest("POST", "machines/"+uuid+"/actions/"+action, bytes.NewBuffer(jp)) - if err != nil { - return err - } - - request.Header.Set("Content-Type", "application/json") - if response, err := c.netClient.Do(request); err != nil { - log.Printf("[DEBUG] [machineDo] call %s:%s error = %v\n", uuid, action, err) - return err - } else { - defer response.Body.Close() - - // We aren't authorized - if response.StatusCode == http.StatusUnauthorized || response.StatusCode == http.StatusForbidden { - log.Printf("[DEBUG] [machineDo] unauthorized %s:%s\n", uuid, action) - return fmt.Errorf("getToken: Unauthorized access") - } - - // We got an error - if response.StatusCode > 299 || response.StatusCode < 200 { - berr := models.Error{} - if err := json.NewDecoder(response.Body).Decode(&berr); err != nil { - log.Printf("[DEBUG] [machineDo] returned %s:%s error = %v\n", uuid, action, err) - return err - } else { - log.Printf("[DEBUG] [machineDo] returned %s:%s error = %v\n", uuid, action, &berr) - return &berr - } - } - } - return nil -} diff --git a/drp/README.md b/drp/README.md new file mode 100644 index 0000000..4dbd8ae --- /dev/null +++ b/drp/README.md @@ -0,0 +1,9 @@ + +The following objects are not reflected: + +* interfaces +* jobs +* leases +* plugin_providers +* preferences + diff --git a/drp/config.go b/drp/config.go new file mode 100644 index 0000000..a80578f --- /dev/null +++ b/drp/config.go @@ -0,0 +1,39 @@ +package drp + +import ( + "log" + + "github.com/digitalrebar/provision/api" +) + +type Config struct { + Token string + Username string + Password string + Url string + + session *api.Client +} + +/* + * Builds a client object for this config + */ +func (c *Config) validateAndConnect() error { + log.Println("[DEBUG] [Config.validateAndConnect] Configuring the DRP API client") + + if c.session != nil { + return nil + } + var err error + if c.Token != "" { + c.session, err = api.TokenSession(c.Url, c.Token) + } else { + c.session, err = api.UserSession(c.Url, c.Username, c.Password) + } + if err != nil { + log.Printf("[ERROR] Error creating session: %v", err) + return err + } + + return nil +} diff --git a/drp/provider.go b/drp/provider.go new file mode 100644 index 0000000..e3894b3 --- /dev/null +++ b/drp/provider.go @@ -0,0 +1,123 @@ +package drp + +import ( + "fmt" + "log" + "os" + "strings" + + "github.com/digitalrebar/provision/models" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +/* + * Enable terraform to use DRP as a provider. Fill out the + * appropriate functions and information about this plugin. + */ +func Provider() terraform.ResourceProvider { + log.Println("[DEBUG] Initializing the DRP provider") + p := &schema.Provider{ + Schema: map[string]*schema.Schema{ + "api_key": { + Type: schema.TypeString, + Optional: true, + Description: "The api key for API operations", + DefaultFunc: schema.EnvDefaultFunc("RS_TOKEN", nil), + }, + "api_user": { + Type: schema.TypeString, + Optional: true, + Description: "The api user for API operations", + DefaultFunc: envDefaultKeyFunc("RS_KEY", "username"), + }, + "api_password": { + Type: schema.TypeString, + Optional: true, + Description: "The api password for API operations", + DefaultFunc: envDefaultKeyFunc("RS_KEY", "password"), + }, + "api_url": { + Type: schema.TypeString, + Required: true, + Description: "The DRP server URL. ie: https://1.2.3.4:8092", + DefaultFunc: schema.EnvDefaultFunc("RS_ENDPOINT", nil), + }, + }, + + ResourcesMap: map[string]*schema.Resource{ + "drp_machine": resourceMachine(), + }, + + ConfigureFunc: providerConfigure, + } + + for _, m := range models.All() { + pref := m.Prefix() + // These are generally read-only. preferences is the one to come. + if pref == "preferences" || pref == "plugin_providers" || + pref == "interfaces" || pref == "jobs" || pref == "leases" { + continue + } + + spref := strings.TrimRight(pref, "s") + if pref == "machines" { + // Machine is already added, add raw_machine to manipulate raw machine objects + spref = "raw_machine" + } + p.ResourcesMap[fmt.Sprintf("drp_%s", spref)] = resourceGeneric(pref) + } + + return p +} + +func envDefaultKeyFunc(k, part string) schema.SchemaDefaultFunc { + return func() (interface{}, error) { + if v := os.Getenv(k); v != "" { + parts := strings.SplitN(v, ":", 2) + if len(parts) < 2 { + return nil, fmt.Errorf("RS_KEY has not enough parts") + } + if part == "username" { + return parts[0], nil + } else if part == "password" { + return parts[1], nil + } + return nil, fmt.Errorf("Asking for unknown part of RS_KEY: %s", part) + } + + return nil, nil + } +} + +/* + * The config method that terraform uses to pass information about configuration + * to the plugin. + */ +func providerConfigure(d *schema.ResourceData) (interface{}, error) { + log.Println("[DEBUG] Configuring the DRP provider") + config := Config{ + Url: d.Get("api_url").(string), + } + + if key := d.Get("api_key"); key != nil { + config.Token = key.(string) + } + if user := d.Get("api_user"); user != nil { + config.Username = user.(string) + config.Password = d.Get("api_password").(string) + } + + if config.Token == "" && config.Username == "" { + return nil, fmt.Errorf("drp provider requires either user or token ids") + } + if config.Username != "" && config.Password == "" { + return nil, fmt.Errorf("drp provider requires a password for the specified user") + } + + if err := config.validateAndConnect(); err != nil { + return nil, err + } + + return &config, nil +} diff --git a/drp/provider_test.go b/drp/provider_test.go new file mode 100644 index 0000000..7bbd457 --- /dev/null +++ b/drp/provider_test.go @@ -0,0 +1,118 @@ +package drp + +import ( + "io/ioutil" + "log" + "os" + "testing" + "time" + + "github.com/digitalrebar/provision/api" + "github.com/digitalrebar/provision/server" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" + flags "github.com/jessevdk/go-flags" +) + +var testAccDrpProviders map[string]terraform.ResourceProvider +var testAccDrpProvider *schema.Provider + +func init() { + testAccDrpProvider = Provider().(*schema.Provider) + testAccDrpProviders = map[string]terraform.ResourceProvider{ + "drp": testAccDrpProvider, + } +} + +func TestProvider(t *testing.T) { + if err := Provider().(*schema.Provider).InternalValidate(); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestProvider_impl(t *testing.T) { + var _ terraform.ResourceProvider = Provider() +} + +func testAccDrpPreCheck(t *testing.T) { + os.Setenv("RS_KEY", "rocketskates:r0cketsk8ts") + os.Setenv("RS_ENDPOINT", "https://127.0.0.1:10031") +} + +var tmpDir string +var session *api.Client + +func generateArgs(args []string) *server.ProgOpts { + var c_opts server.ProgOpts + + parser := flags.NewParser(&c_opts, flags.Default) + if _, err := parser.ParseArgs(args); err != nil { + if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp { + os.Exit(0) + } else { + os.Exit(1) + } + } + + return &c_opts +} + +func TestMain(m *testing.M) { + var err error + tmpDir, err = ioutil.TempDir("", "tf-") + if err != nil { + log.Printf("Creating temp dir for file root failed: %v", err) + os.Exit(1) + } + + testArgs := []string{ + "--base-root", tmpDir, + "--tls-key", tmpDir + "/server.key", + "--tls-cert", tmpDir + "/server.crt", + "--api-port", "10031", + "--static-port", "10032", + "--tftp-port", "10033", + "--dhcp-port", "10034", + "--pxe-port", "10035", + "--fake-pinger", + "--drp-id", "Fred", + "--backend", "memory:///", + "--debug-frontend", "0", + "--debug-renderer", "0", + "--debug-plugins", "0", + "--local-content", "", + "--default-content", "", + } + + err = os.MkdirAll(tmpDir+"/plugins", 0755) + if err != nil { + log.Printf("Error creating required directory %s: %v", tmpDir+"/plugins", err) + os.Exit(1) + } + + c_opts := generateArgs(testArgs) + go server.Server(c_opts) + + count := 0 + for count < 30 { + session, err = api.UserSession("https://127.0.0.1:10031", "rocketskates", "r0cketsk8ts") + if err == nil { + break + } + time.Sleep(1 * time.Second) + count++ + } + if session == nil { + log.Printf("Failed to create UserSession: %v", err) + os.RemoveAll(tmpDir) + os.Exit(1) + } + if err != nil { + log.Printf("Server failed to start in time allowed") + os.RemoveAll(tmpDir) + os.Exit(1) + } + ret := m.Run() + os.RemoveAll(tmpDir) + os.Exit(ret) +} diff --git a/drp/resource_drp_bootenv_test.go b/drp/resource_drp_bootenv_test.go new file mode 100644 index 0000000..d6097c9 --- /dev/null +++ b/drp/resource_drp_bootenv_test.go @@ -0,0 +1,174 @@ +package drp + +import ( + "fmt" + "testing" + + "github.com/digitalrebar/provision/models" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +var testAccDrpBootEnv_basic = ` + resource "drp_bootenv" "foo" { + Name = "foo" + Meta = { + "field1" = "value1" + "field2" = "value2" + } + OS = {} + }` + +func TestAccDrpBootEnv_basic(t *testing.T) { + bootenv := models.BootEnv{Name: "foo", + Meta: map[string]string{"field1": "value1", "field2": "value2"}, + } + bootenv.Fill() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccDrpPreCheck(t) }, + Providers: testAccDrpProviders, + CheckDestroy: testAccDrpCheckBootEnvDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDrpBootEnv_basic, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckBootEnvExists(t, "drp_bootenv.foo", &bootenv), + ), + }, + }, + }) +} + +var testAccDrpBootEnv_change_1 = ` + resource "drp_bootenv" "foo" { + Name = "foo" + Description = "I am a bootenv" + RequiredParams = [ "p1", "p2" ] + OptionalParams = [ "p3", "p4" ] + Templates = [ + { Name = "t1", Path = "fred1", Contents = "temp1"}, + { Name = "t2", Path = "fred2", Contents = "actual stuff"} + ] + Kernel = "jill" + Initrds = [ "joyce", "julie", "janna" ] + BootParams = "kernel go" + OnlyUnknown = true + OS = {} + }` + +var testAccDrpBootEnv_change_2 = ` + resource "drp_bootenv" "foo" { + Name = "foo" + Description = "I am a bootenv again" + RequiredParams = [ "p3", "p4" ] + OptionalParams = [ "p1", "p2" ] + Templates = [ + { Name = "t3", Path = "jill1", Contents = "temp2"}, + { Name = "t4", Path = "jill2", Contents = "really actual stuff"} + ] + Kernel = "~jill" + Initrds = [ "~joyce", "~julie", "~janna" ] + BootParams = "kernel nogo" + OnlyUnknown = false + OS = {} + }` + +func TestAccDrpBootEnv_change(t *testing.T) { + bootenv1 := models.BootEnv{ + Name: "foo", + Description: "I am a bootenv", + RequiredParams: []string{"p1", "p2"}, + OptionalParams: []string{"p3", "p4"}, + Templates: []models.TemplateInfo{ + {Name: "t1", Path: "fred1", Contents: "temp1"}, + {Name: "t2", Path: "fred2", Contents: "actual stuff"}, + }, + Kernel: "jill", + Initrds: []string{"joyce", "julie", "janna"}, + BootParams: "kernel go", + OnlyUnknown: true, + } + bootenv1.Fill() + bootenv2 := models.BootEnv{ + Name: "foo", + Description: "I am a bootenv again", + RequiredParams: []string{"p3", "p4"}, + OptionalParams: []string{"p1", "p2"}, + Templates: []models.TemplateInfo{ + {Name: "t3", Path: "jill1", Contents: "temp2"}, + {Name: "t4", Path: "jill2", Contents: "really actual stuff"}, + }, + Kernel: "~jill", + Initrds: []string{"~joyce", "~julie", "~janna"}, + BootParams: "kernel nogo", + } + bootenv2.Fill() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccDrpPreCheck(t) }, + Providers: testAccDrpProviders, + CheckDestroy: testAccDrpCheckBootEnvDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDrpBootEnv_change_1, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckBootEnvExists(t, "drp_bootenv.foo", &bootenv1), + ), + }, + { + Config: testAccDrpBootEnv_change_2, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckBootEnvExists(t, "drp_bootenv.foo", &bootenv2), + ), + }, + }, + }) +} + +func testAccDrpCheckBootEnvDestroy(s *terraform.State) error { + config := testAccDrpProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "drp_bootenv" { + continue + } + + if _, err := config.session.GetModel("bootenvs", rs.Primary.ID); err == nil { + return fmt.Errorf("BootEnv still exists") + } + } + + return nil +} + +func testAccDrpCheckBootEnvExists(t *testing.T, n string, bootenv *models.BootEnv) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccDrpProvider.Meta().(*Config) + + obj, err := config.session.GetModel("bootenvs", rs.Primary.ID) + if err != nil { + return err + } + found := obj.(*models.BootEnv) + found.ClearValidation() + + if found.Name != rs.Primary.ID { + return fmt.Errorf("BootEnv not found") + } + + if err := diffObjects(bootenv, found, "BootEnv"); err != nil { + return err + } + return nil + } +} diff --git a/drp/resource_drp_machine.go b/drp/resource_drp_machine.go new file mode 100644 index 0000000..64f44a2 --- /dev/null +++ b/drp/resource_drp_machine.go @@ -0,0 +1,392 @@ +package drp + +import ( + "bytes" + "fmt" + "log" + "time" + + "github.com/digitalrebar/provision/models" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceMachine() *schema.Resource { + log.Println("[DEBUG] [resourceMachine] Initializing data structure") + + m, _ := models.New("machine") + r := buildSchema(m) + + r.Create = resourceMachineCreate + r.Update = resourceMachineUpdate + r.Delete = resourceMachineDelete + + // Define what the machines completion stage. + r.Schema["completion_stage"] = &schema.Schema{ + Type: schema.TypeString, + Optional: true, + } + + // Define what the machines decommision stage + r.Schema["decommission_stage"] = &schema.Schema{ + Type: schema.TypeString, + Optional: true, + } + + // Define what profiles to add and remove at destroy + r.Schema["add_profiles"] = &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + } + + // Machines also have filters + r.Schema["filters"] = &schema.Schema{ + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Optional: true, + }, + "jsonvalue": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + } + return r +} + +func allocateMachine(cc *Config, filters []string) (*models.Machine, error) { + for { + machines, err := cc.session.ListModel("machine", filters...) + if err != nil { + return nil, err + } + if len(machines) == 0 { + return nil, fmt.Errorf("No machines available") + } + + machine := machines[0] + merged := models.Clone(machine).(*models.Machine) + merged.Params["terraform/allocated"] = true + + ret, err := cc.session.PatchTo(machine, merged) + if err != nil { + berr, ok := err.(*models.Error) + if ok { + // If we get a patch error, the machine was allocated while we were + // waiting. Try again. + if berr.Type == "PATCH" && (berr.Code == 406 || berr.Code == 409) { + continue + } + } + return nil, err + } + return ret.(*models.Machine), nil + } +} + +func releaseMachine(cc *Config, uuid string, tfManaged bool) error { + for { + machine, err := cc.session.GetModel("machines", uuid) + if err != nil { + return nil + } + + merged := models.Clone(machine).(*models.Machine) + merged.Params["terraform/allocated"] = false + merged.Params["terraform/managed"] = tfManaged + + _, err = cc.session.PatchTo(machine, merged) + if err != nil { + berr, ok := err.(*models.Error) + if ok { + // If we get a patch error, the machine was allocated while we were + // waiting. Try again. + if berr.Type == "PATCH" && (berr.Code == 406 || berr.Code == 409) { + continue + } + } + return err + } + return nil + } +} + +func getMachineStatus(cc *Config, uuid string, stages []string) resource.StateRefreshFunc { + log.Printf("[DEBUG] [getMachineStatus] Getting status of machine: %s", uuid) + return func() (interface{}, string, error) { + mo, err := cc.session.GetModel("machines", uuid) + if err != nil { + log.Printf("[ERROR] [getMachineStatus] Unable to get machine: %s\n", uuid) + return nil, "", err + } + machineObject := mo.(*models.Machine) + + // 6 == done 9 == pending + machineStatus := "6" + if machineObject.Stage != "" { + found := false + for _, s := range stages { + if s == machineObject.Stage { + found = true + break + } + } + if !found { + machineStatus = "9" + } + } else { + if machineObject.BootEnv != "local" { + machineStatus = "9" + } + } + + var statusRetVal bytes.Buffer + statusRetVal.WriteString(machineStatus) + statusRetVal.WriteString(":") + + return machineObject, statusRetVal.String(), nil + } +} + +func machineDo(cc *Config, uuid, action string) error { + log.Printf("[DEBUG] [machineDo] uuid: %s, action: %s", uuid, action) + + actionParams := map[string]interface{}{} + var resp interface{} + err := cc.session.Req().Post(actionParams).UrlFor("machines", uuid, "actions", action).Do(resp) + if err != nil { + log.Printf("[DEBUG] [machineDo] call %s:%s error = %v\n", uuid, action, err) + return err + } + return nil +} + +func updateMachine(cc *Config, machineObj *models.Machine, d *schema.ResourceData) (*models.Machine, error) { + obj, e := buildModel(machineObj, d) + if e != nil { + log.Printf("[ERROR] [updateMachine] Unable to build model: %v\n", e) + return nil, e + } + m := obj.(*models.Machine) + + // Make sure the add profiles are on the machine + if ol, ok := d.GetOk("add_profiles"); ok { + l := ol.([]interface{}) + for _, s := range l { + prof := s.(string) + + found := false + for _, t := range m.Profiles { + if t == prof { + found = true + break + } + } + + if !found { + m.Profiles = append(m.Profiles, prof) + } + } + } + + cBootEnv := machineObj.BootEnv + + err := cc.session.Req().PatchTo(machineObj, m).Params("force", "true").Do(&m) + if err != nil { + log.Printf("[ERROR] [updateMachine] Unable to initialize machine: %v\n", err) + return nil, err + } + machineObj = m + + if err := machineDo(cc, machineObj.UUID(), "nextbootpxe"); err != nil { + log.Printf("[WARN] [updateMachine] Unable to mark the machine for pxe next boot: %s\n", machineObj.UUID()) + } + + // Power on and then cycle, if needed + if err := machineDo(cc, machineObj.UUID(), "poweron"); err != nil { + log.Printf("[WARN] [updateMachine] Unable to power on machine: %s\n", machineObj.UUID()) + } + + obj, err = cc.session.GetModel(machineObj.Prefix(), machineObj.Key()) + if err != nil { + log.Printf("[ERROR] [updateMachine] Unable to re-get machine: %v\n", err) + return nil, err + } + machineObj = obj.(*models.Machine) + + if machineObj.BootEnv != cBootEnv { + if err := machineDo(cc, machineObj.Key(), "powercycle"); err != nil { + log.Printf("[WARN] [updateMachine] Unable to power cycleup machine: %s\n", machineObj.UUID()) + } + } + + return machineObj, nil +} + +// This function doesn't really *create* a new machine but, +// consume an already registered machine. +func resourceMachineCreate(d *schema.ResourceData, meta interface{}) error { + log.Println("[DEBUG] [resourceMachineCreate] Launching new drp_machine") + cc := meta.(*Config) + + filters := []string{} + if pval, set := d.GetOk("filters"); set { + for _, o := range pval.([]interface{}) { + v := o.(map[string]interface{}) + filters = append(filters, v["name"].(string), v["value"].(string)) + } + } + filters = append(filters, "terraform/allocated", "false") + filters = append(filters, "terraform/managed", "true") + + machineObj, err := allocateMachine(cc, filters) + if err != nil { + log.Printf("[ERROR] [resourceMachineCreate] Unable to allocate machine: %v\n", err) + return err + } + + uuid := machineObj.UUID() + machineObj, err = updateMachine(cc, machineObj, d) + if err != nil { + log.Printf("[ERROR] [resourceMachineCreate] Unable to update machine: %v\n", err) + if err2 := releaseMachine(cc, uuid, true); err2 != nil { + log.Printf("[ERROR] [resourceMachineCreate] Unable to release machine: %v\n", err2) + } + return err + } + + log.Printf("[DEBUG] [resourceMachineCreate] Waiting for machine (%s) to become active\n", machineObj.UUID()) + + stages := []string{"complete", "complete-nowait"} + if ns, ok := d.GetOk("completion_stage"); ok { + stages = []string{ns.(string)} + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{"9:"}, + Target: []string{"6:"}, + Refresh: getMachineStatus(cc, machineObj.UUID(), stages), + Timeout: 25 * time.Minute, + Delay: 10 * time.Second, + MinTimeout: 3 * time.Second, + } + + if _, err := stateConf.WaitForState(); err != nil { + if err2 := releaseMachine(cc, machineObj.UUID(), true); err2 != nil { + log.Printf("[ERROR] [resourceMachineCreate] Unable to release machine: %v\n", err2) + } + return fmt.Errorf( + "[ERROR] [resourceMachineCreate] Error waiting for machine (%s) to become deployed: %s", + machineObj.UUID(), err) + } + + d.SetId(machineObj.UUID()) + + answer, err := cc.session.GetModel(machineObj.Prefix(), d.Id()) + if err != nil { + return err + } + return updateResourceData(answer, d) +} + +func resourceMachineUpdate(d *schema.ResourceData, meta interface{}) error { + cc := meta.(*Config) + log.Printf("[DEBUG] [resourceMachineUpdate] Modifying machine %s\n", d.Id()) + + obj, err := cc.session.GetModel("machines", d.Id()) + if err != nil { + log.Printf("[ERROR] [resourceMachineUpdate] Unable to get machine: %v\n", err) + return err + } + machineObj := obj.(*models.Machine) + + machineObj, err = updateMachine(cc, machineObj, d) + if err != nil { + log.Printf("[ERROR] [resourceMachineCreate] Unable to update machine: %v\n", err) + return err + } + + log.Printf("[DEBUG] Done Modifying machine %s\n", d.Id()) + return updateResourceData(machineObj, d) +} + +// This function doesn't really *delete* a drp managed machine but releases (read, turns off) the machine. +func resourceMachineDelete(d *schema.ResourceData, meta interface{}) error { + cc := meta.(*Config) + log.Printf("[DEBUG] Deleting machine %s\n", d.Id()) + + obj, err := cc.session.GetModel("machines", d.Id()) + if err != nil { + log.Printf("[ERROR] [resourceMachineDelete] Failed to get machine: %v\n", err) + return err + } + machineObj := obj.(*models.Machine) + newObj := models.Clone(machineObj).(*models.Machine) + + if ns, ok := d.GetOk("decommission_stage"); ok { + newObj.Stage = ns.(string) + } else { + if machineObj.Stage != "" { + newObj.Stage = "discover" + } else { + newObj.BootEnv = "sledgehammer" + } + } + // Since we are rebooting, set not runnable. + newObj.Runnable = false + + // Remove the profiles + if ol, ok := d.GetOk("add_profiles"); ok { + l := ol.([]interface{}) + newList := []string{} + + for _, ts := range newObj.Profiles { + found := false + for _, s := range l { + prof := s.(string) + if prof == ts { + found = true + break + } + } + if !found { + newList = append(newList, ts) + } + } + + newObj.Profiles = newList + } + + // Update the machine to request position + err = cc.session.Req().PatchTo(machineObj, newObj).Params("force", "true").Do(&newObj) + if err != nil { + log.Printf("[ERROR] [resourceMachineDelete] Unable to reset machine: %v\n", err) + return err + } + + if err := releaseMachine(cc, d.Id(), false); err != nil { + return err + } + + if err := machineDo(cc, machineObj.UUID(), "nextbootpxe"); err != nil { + log.Printf("[ERROR] [resourceMachineRelease] Unable to mark the machine for pxe next boot: %s\n", machineObj.UUID()) + } + if err := machineDo(cc, machineObj.UUID(), "powercycle"); err != nil { + log.Printf("[ERROR] [resourceMachineRelease] Unable to power cycle machine: %s\n", machineObj.UUID()) + } + + log.Printf("[DEBUG] [resourceMachineDelete] Machine (%s) released", d.Id()) + + d.SetId("") + + return nil +} diff --git a/drp/resource_drp_machine_test.go b/drp/resource_drp_machine_test.go new file mode 100644 index 0000000..8482159 --- /dev/null +++ b/drp/resource_drp_machine_test.go @@ -0,0 +1,119 @@ +package drp + +import ( + "fmt" + "testing" + + "github.com/digitalrebar/provision/models" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/pborman/uuid" +) + +var testAccDrpMachine_basic = ` + resource "drp_machine" "foo" { + Name = "mach1" + Stage = "local" + completion_stage = "local" + decommission_stage = "none" + add_profiles = [ "p-test" ] + Meta = { + "feature-flags" = "change-stage-v2" + "field1" = "value1" + "field2" = "value2" + } + }` + +func TestAccDrpMachine_basic(t *testing.T) { + machine := models.Machine{ + Name: "mach1", + Uuid: uuid.Parse("3945838b-be8c-4b35-8b1c-b538ddc71f7e"), + Secret: "12", + Runnable: true, + BootEnv: "local", + Stage: "local", + Profiles: []string{"p-test"}, + Params: map[string]interface{}{ + "terraform/allocated": true, + "terraform/managed": true, + }, + Meta: map[string]string{"feature-flags": "change-stage-v2", "field1": "value1", "field2": "value2"}, + } + machine.Fill() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccDrpPreCheck(t) }, + Providers: testAccDrpProviders, + CheckDestroy: testAccDrpCheckMachineDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + PreConfig: testAccCreateResources, + Config: testAccDrpMachine_basic, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckMachineExists(t, "drp_machine.foo", &machine), + ), + }, + }, + }) +} + +func testAccCreateResources() { + config := testAccDrpProvider.Meta().(*Config) + + p := &models.Profile{Name: "p-test"} + ta := &models.Param{Name: "terraform/allocated", Schema: map[string]string{"type": "boolean"}} + tm := &models.Param{Name: "terraform/managed", Schema: map[string]string{"type": "boolean"}} + m := &models.Machine{Name: "mach1", Secret: "12", Params: map[string]interface{}{"terraform/allocated": false, "terraform/managed": true}, Uuid: uuid.Parse("3945838b-be8c-4b35-8b1c-b538ddc71f7e")} + + config.session.CreateModel(p) + config.session.CreateModel(ta) + config.session.CreateModel(tm) + config.session.CreateModel(m) +} + +func testAccDrpCheckMachineDestroy(s *terraform.State) error { + config := testAccDrpProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "drp_machine" { + continue + } + + if _, err := config.session.GetModel("machines", rs.Primary.ID); err != nil { + return fmt.Errorf("Machine does not exist") + } + } + + return nil +} + +func testAccDrpCheckMachineExists(t *testing.T, n string, machine *models.Machine) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccDrpProvider.Meta().(*Config) + + obj, err := config.session.GetModel("machines", rs.Primary.ID) + if err != nil { + return err + } + found := obj.(*models.Machine) + found.ClearValidation() + + if found.Key() != rs.Primary.ID { + return fmt.Errorf("Machine not found") + } + + if err := diffObjects(machine, found, "Machine"); err != nil { + return err + } + return nil + } +} diff --git a/drp/resource_drp_param_test.go b/drp/resource_drp_param_test.go new file mode 100644 index 0000000..f5b67b1 --- /dev/null +++ b/drp/resource_drp_param_test.go @@ -0,0 +1,140 @@ +package drp + +import ( + "fmt" + "testing" + + "github.com/digitalrebar/provision/models" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +var testAccDrpParam_basic = ` + resource "drp_param" "foo" { + Name = "foo" + Meta = { + "field1" = "value1" + "field2" = "value2" + } + }` + +func TestAccDrpParam_basic(t *testing.T) { + param := models.Param{Name: "foo", + Meta: map[string]string{"field1": "value1", "field2": "value2"}, + } + param.Fill() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccDrpPreCheck(t) }, + Providers: testAccDrpProviders, + CheckDestroy: testAccDrpCheckParamDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDrpParam_basic, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckParamExists(t, "drp_param.foo", ¶m), + ), + }, + }, + }) +} + +var testAccDrpParam_change_1 = ` + resource "drp_param" "foo" { + Name = "foo" + Description = "I am a param" + Documentation = "here I am" + Schema = "{\"type\":\"boolean\"}" + }` + +var testAccDrpParam_change_2 = ` + resource "drp_param" "foo" { + Name = "foo" + Description = "I am a param again" + Documentation = "here am I" + Schema = "{\"type\":\"integer\"}" + }` + +func TestAccDrpParam_change(t *testing.T) { + param1 := models.Param{ + Name: "foo", + Description: "I am a param", + Documentation: "here I am", + Schema: map[string]string{"type": "boolean"}, + } + param1.Fill() + param2 := models.Param{ + Name: "foo", + Description: "I am a param again", + Documentation: "here am I", + Schema: map[string]string{"type": "integer"}, + } + param2.Fill() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccDrpPreCheck(t) }, + Providers: testAccDrpProviders, + CheckDestroy: testAccDrpCheckParamDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDrpParam_change_1, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckParamExists(t, "drp_param.foo", ¶m1), + ), + }, + resource.TestStep{ + Config: testAccDrpParam_change_2, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckParamExists(t, "drp_param.foo", ¶m2), + ), + }, + }, + }) +} + +func testAccDrpCheckParamDestroy(s *terraform.State) error { + config := testAccDrpProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "drp_param" { + continue + } + + if _, err := config.session.GetModel("params", rs.Primary.ID); err == nil { + return fmt.Errorf("Param still exists") + } + } + + return nil +} + +func testAccDrpCheckParamExists(t *testing.T, n string, param *models.Param) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccDrpProvider.Meta().(*Config) + + obj, err := config.session.GetModel("params", rs.Primary.ID) + if err != nil { + return err + } + found := obj.(*models.Param) + found.ClearValidation() + + if found.Name != rs.Primary.ID { + return fmt.Errorf("Param not found") + } + + if err := diffObjects(param, found, "Param"); err != nil { + return err + } + return nil + } +} diff --git a/drp/resource_drp_plugin_test.go b/drp/resource_drp_plugin_test.go new file mode 100644 index 0000000..1cce653 --- /dev/null +++ b/drp/resource_drp_plugin_test.go @@ -0,0 +1,168 @@ +package drp + +import ( + "fmt" + "testing" + + "github.com/digitalrebar/provision/models" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +var testAccDrpPlugin_basic = ` + resource "drp_plugin" "foo" { + Name = "foo" + PluginProvider = "ipmi" + Meta = { + "field1" = "value1" + "field2" = "value2" + } + }` + +func TestAccDrpPlugin_basic(t *testing.T) { + plugin := models.Plugin{Name: "foo", + Provider: "ipmi", + Meta: map[string]string{"field1": "value1", "field2": "value2"}, + } + plugin.Fill() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccDrpPreCheck(t) }, + Providers: testAccDrpProviders, + CheckDestroy: testAccDrpCheckPluginDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDrpPlugin_basic, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckPluginExists(t, "drp_plugin.foo", &plugin), + ), + }, + }, + }) +} + +var testAccDrpPlugin_change_1 = ` + resource "drp_plugin" "foo" { + Name = "foo" + PluginProvider = "ipmi" + Description = "I am a plugin" + }` + +var testAccDrpPlugin_change_2 = ` + resource "drp_plugin" "foo" { + Name = "foo" + PluginProvider = "ipmi" + Description = "I am a plugin again" + }` + +func TestAccDrpPlugin_change(t *testing.T) { + plugin1 := models.Plugin{Name: "foo", Description: "I am a plugin", Provider: "ipmi"} + plugin1.Fill() + plugin2 := models.Plugin{Name: "foo", Description: "I am a plugin again", Provider: "ipmi"} + plugin2.Fill() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccDrpPreCheck(t) }, + Providers: testAccDrpProviders, + CheckDestroy: testAccDrpCheckPluginDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDrpPlugin_change_1, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckPluginExists(t, "drp_plugin.foo", &plugin1), + ), + }, + resource.TestStep{ + Config: testAccDrpPlugin_change_2, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckPluginExists(t, "drp_plugin.foo", &plugin2), + ), + }, + }, + }) +} + +var testAccDrpPlugin_withParams = ` + resource "drp_plugin" "foo" { + Name = "foo" + PluginProvider = "ipmi" + Params = { + "test/string" = "fred" + "test/int" = 3 + "test/bool" = "true" + "test/list" = "[\"one\",\"two\"]" + } + }` + +func TestAccDrpPlugin_withParams(t *testing.T) { + plugin := models.Plugin{Name: "foo", Provider: "ipmi", + Params: map[string]interface{}{ + "test/string": "fred", + "test/int": 3, + "test/bool": true, + "test/list": []string{"one", "two"}, + }, + } + plugin.Fill() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccDrpPreCheck(t) }, + Providers: testAccDrpProviders, + CheckDestroy: testAccDrpCheckPluginDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDrpPlugin_withParams, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckPluginExists(t, "drp_plugin.foo", &plugin), + ), + }, + }, + }) +} + +func testAccDrpCheckPluginDestroy(s *terraform.State) error { + config := testAccDrpProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "drp_plugin" { + continue + } + + if _, err := config.session.GetModel("plugins", rs.Primary.ID); err == nil { + return fmt.Errorf("Plugin still exists") + } + } + + return nil +} + +func testAccDrpCheckPluginExists(t *testing.T, n string, plugin *models.Plugin) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccDrpProvider.Meta().(*Config) + + obj, err := config.session.GetModel("plugins", rs.Primary.ID) + if err != nil { + return err + } + found := obj.(*models.Plugin) + found.ClearValidation() + + if found.Name != rs.Primary.ID { + return fmt.Errorf("Plugin not found") + } + + if err := diffObjects(plugin, found, "Plugin"); err != nil { + return err + } + return nil + } +} diff --git a/drp/resource_drp_profile_test.go b/drp/resource_drp_profile_test.go new file mode 100644 index 0000000..b535572 --- /dev/null +++ b/drp/resource_drp_profile_test.go @@ -0,0 +1,164 @@ +package drp + +import ( + "fmt" + "testing" + + "github.com/digitalrebar/provision/models" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +var testAccDrpProfile_basic = ` + resource "drp_profile" "foo" { + Name = "foo" + Meta = { + "field1" = "value1" + "field2" = "value2" + } + }` + +func TestAccDrpProfile_basic(t *testing.T) { + profile := models.Profile{Name: "foo", + Meta: map[string]string{"field1": "value1", "field2": "value2"}, + } + profile.Fill() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccDrpPreCheck(t) }, + Providers: testAccDrpProviders, + CheckDestroy: testAccDrpCheckProfileDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDrpProfile_basic, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckProfileExists(t, "drp_profile.foo", &profile), + ), + }, + }, + }) +} + +var testAccDrpProfile_change_1 = ` + resource "drp_profile" "foo" { + Name = "foo" + Description = "I am a profile" + }` + +var testAccDrpProfile_change_2 = ` + resource "drp_profile" "foo" { + Name = "foo" + Description = "I am a profile again" + }` + +func TestAccDrpProfile_change(t *testing.T) { + profile1 := models.Profile{Name: "foo", Description: "I am a profile"} + profile1.Fill() + profile2 := models.Profile{Name: "foo", Description: "I am a profile again"} + profile2.Fill() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccDrpPreCheck(t) }, + Providers: testAccDrpProviders, + CheckDestroy: testAccDrpCheckProfileDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDrpProfile_change_1, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckProfileExists(t, "drp_profile.foo", &profile1), + ), + }, + resource.TestStep{ + Config: testAccDrpProfile_change_2, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckProfileExists(t, "drp_profile.foo", &profile2), + ), + }, + }, + }) +} + +var testAccDrpProfile_withParams = ` + resource "drp_profile" "foo" { + Name = "foo" + Description = "I am a profile again" + Params = { + "test/string" = "fred" + "test/int" = 3 + "test/bool" = "true" + "test/list" = "[\"one\",\"two\"]" + } + }` + +func TestAccDrpProfile_withParams(t *testing.T) { + profile := models.Profile{Name: "foo", Description: "I am a profile again", + Params: map[string]interface{}{ + "test/string": "fred", + "test/int": 3, + "test/bool": true, + "test/list": []string{"one", "two"}, + }, + } + profile.Fill() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccDrpPreCheck(t) }, + Providers: testAccDrpProviders, + CheckDestroy: testAccDrpCheckProfileDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDrpProfile_withParams, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckProfileExists(t, "drp_profile.foo", &profile), + ), + }, + }, + }) +} + +func testAccDrpCheckProfileDestroy(s *terraform.State) error { + config := testAccDrpProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "drp_profile" { + continue + } + + if _, err := config.session.GetModel("profiles", rs.Primary.ID); err == nil { + return fmt.Errorf("Profile still exists") + } + } + + return nil +} + +func testAccDrpCheckProfileExists(t *testing.T, n string, profile *models.Profile) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccDrpProvider.Meta().(*Config) + + obj, err := config.session.GetModel("profiles", rs.Primary.ID) + if err != nil { + return err + } + found := obj.(*models.Profile) + found.ClearValidation() + + if found.Name != rs.Primary.ID { + return fmt.Errorf("Profile not found") + } + + if err := diffObjects(profile, found, "Profile"); err != nil { + return err + } + return nil + } +} diff --git a/drp/resource_drp_raw_machine_test.go b/drp/resource_drp_raw_machine_test.go new file mode 100644 index 0000000..8b16982 --- /dev/null +++ b/drp/resource_drp_raw_machine_test.go @@ -0,0 +1,270 @@ +package drp + +import ( + "fmt" + "net" + "testing" + + "github.com/digitalrebar/provision/models" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/pborman/uuid" +) + +var testAccDrpRawMachine_basic = ` + resource "drp_raw_machine" "foo" { + Name = "mach11" + Uuid = "3945838b-be8c-4b35-8b1c-b538ddc71f7c" + Secret = "12" + Meta = { + "field1" = "value1" + "field2" = "value2" + "feature-flags" = "change-stage-v2" + } + }` + +func TestAccDrpRawMachine_basic(t *testing.T) { + raw_machine := models.Machine{Name: "mach11", + Uuid: uuid.Parse("3945838b-be8c-4b35-8b1c-b538ddc71f7c"), + Secret: "12", + Stage: "none", + BootEnv: "local", + Runnable: true, + Meta: map[string]string{"feature-flags": "change-stage-v2", "field1": "value1", "field2": "value2"}, + } + raw_machine.Fill() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccDrpPreCheck(t) }, + Providers: testAccDrpProviders, + CheckDestroy: testAccDrpCheckRawMachineDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDrpRawMachine_basic, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckRawMachineExists(t, "drp_raw_machine.foo", &raw_machine), + ), + }, + }, + }) +} + +var testAccDrpRawMachine_change_1 = ` + resource "drp_profile" "p1" { + Name = "p1" + } + resource "drp_profile" "p2" { + Name = "p2" + } + resource "drp_task" "t1" { + Name = "t1" + } + resource "drp_task" "t2" { + Name = "t2" + } + resource "drp_raw_machine" "foo" { + depends_on = ["drp_profile.p1", "drp_profile.p2", "drp_task.t1", "drp_task.t2"] + Name = "mach11" + Uuid = "3945838b-be8c-4b35-8b1c-b538ddc71f7c" + Secret = "12" + Description = "I am a raw_machine" + CurrentJob = "3945838b-be8c-4b35-8b1c-b538ddc71f7f" + Address = "1.1.1.1" + Stage = "none" + BootEnv = "local" + Profiles = [ "p1", "p2" ] + Tasks = [ "t1", "t2" ] + Runnable = true + OS = "fred" + }` + +var testAccDrpRawMachine_change_2 = ` + resource "drp_profile" "p1" { + Name = "p1" + } + resource "drp_profile" "p2" { + Name = "p2" + } + resource "drp_task" "t1" { + Name = "t1" + } + resource "drp_task" "t2" { + Name = "t2" + } + resource "drp_profile" "p3" { + Name = "p3" + } + resource "drp_profile" "p4" { + Name = "p4" + } + resource "drp_task" "t3" { + Name = "t3" + } + resource "drp_task" "t4" { + Name = "t4" + } + resource "drp_raw_machine" "foo" { + depends_on = ["drp_profile.p3", "drp_profile.p4", "drp_task.t3", "drp_task.t4", "drp_profile.p1", "drp_profile.p2", "drp_task.t1", "drp_task.t2"] + Name = "mach11" + Uuid = "3945838b-be8c-4b35-8b1c-b538ddc71f7c" + Secret = "12" + Description = "I am a raw_machine again" + CurrentJob = "3945838b-be8c-4b35-8b1c-b538ddc71f7a" + Address = "1.1.1.2" + Stage = "none" + BootEnv = "local" + Profiles = [ "p3", "p4" ] + Tasks = [ "t3", "t4" ] + Runnable = false + OS = "greg" + }` + +func TestAccDrpRawMachine_change(t *testing.T) { + raw_machine1 := models.Machine{ + Name: "mach11", + Address: net.ParseIP("1.1.1.1"), + Description: "I am a raw_machine", + Uuid: uuid.Parse("3945838b-be8c-4b35-8b1c-b538ddc71f7c"), + CurrentJob: uuid.Parse("3945838b-be8c-4b35-8b1c-b538ddc71f7f"), + CurrentTask: -1, + Secret: "12", + Stage: "none", + BootEnv: "local", + Runnable: true, + Profiles: []string{"p1", "p2"}, + Tasks: []string{"t1", "t2"}, + OS: "fred", + Meta: map[string]string{"feature-flags": "change-stage-v2"}, + } + raw_machine1.Fill() + raw_machine2 := models.Machine{ + Name: "mach11", + Address: net.ParseIP("1.1.1.2"), + Description: "I am a raw_machine again", + Uuid: uuid.Parse("3945838b-be8c-4b35-8b1c-b538ddc71f7c"), + CurrentJob: uuid.Parse("3945838b-be8c-4b35-8b1c-b538ddc71f7a"), + CurrentTask: -1, + Secret: "12", + Stage: "none", + BootEnv: "local", + Profiles: []string{"p3", "p4"}, + Tasks: []string{"t3", "t4"}, + Runnable: false, + OS: "greg", + Meta: map[string]string{"feature-flags": "change-stage-v2"}, + } + raw_machine2.Fill() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccDrpPreCheck(t) }, + Providers: testAccDrpProviders, + CheckDestroy: testAccDrpCheckRawMachineDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDrpRawMachine_change_1, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckRawMachineExists(t, "drp_raw_machine.foo", &raw_machine1), + ), + }, + resource.TestStep{ + Config: testAccDrpRawMachine_change_2, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckRawMachineExists(t, "drp_raw_machine.foo", &raw_machine2), + ), + }, + }, + }) +} + +var testAccDrpRawMachine_withParams = ` + resource "drp_raw_machine" "foo" { + Name = "mach11" + Uuid = "3945838b-be8c-4b35-8b1c-b538ddc71f7c" + Secret = "12" + Params = { + "test/string" = "fred" + "test/int" = "3" + "test/bool" = "true" + "test/list" = "[\"one\",\"two\"]" + } + }` + +func TestAccDrpRawMachine_withParams(t *testing.T) { + raw_machine := models.Machine{ + Name: "mach11", + Uuid: uuid.Parse("3945838b-be8c-4b35-8b1c-b538ddc71f7c"), + Secret: "12", + Stage: "none", + BootEnv: "local", + Runnable: true, + Meta: map[string]string{"feature-flags": "change-stage-v2"}, + Params: map[string]interface{}{ + "test/string": "fred", + "test/int": 3, + "test/bool": true, + "test/list": []string{"one", "two"}, + }, + } + raw_machine.Fill() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccDrpPreCheck(t) }, + Providers: testAccDrpProviders, + CheckDestroy: testAccDrpCheckRawMachineDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDrpRawMachine_withParams, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckRawMachineExists(t, "drp_raw_machine.foo", &raw_machine), + ), + }, + }, + }) +} + +func testAccDrpCheckRawMachineDestroy(s *terraform.State) error { + config := testAccDrpProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "drp_raw_machine" { + continue + } + + if _, err := config.session.GetModel("raw_machines", rs.Primary.ID); err == nil { + return fmt.Errorf("RawMachine still exists") + } + } + + return nil +} + +func testAccDrpCheckRawMachineExists(t *testing.T, n string, raw_machine *models.Machine) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccDrpProvider.Meta().(*Config) + + obj, err := config.session.GetModel("machines", rs.Primary.ID) + if err != nil { + return err + } + found := obj.(*models.Machine) + found.ClearValidation() + + if found.Uuid.String() != rs.Primary.ID { + return fmt.Errorf("RawMachine not found") + } + + if err := diffObjects(raw_machine, found, "RawMachine"); err != nil { + return err + } + return nil + } +} diff --git a/drp/resource_drp_reservation_test.go b/drp/resource_drp_reservation_test.go new file mode 100644 index 0000000..f7e5c01 --- /dev/null +++ b/drp/resource_drp_reservation_test.go @@ -0,0 +1,163 @@ +package drp + +import ( + "fmt" + "net" + "testing" + + "github.com/digitalrebar/provision/models" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +var testAccDrpReservation_basic = ` + resource "drp_reservation" "foo" { + Addr = "1.1.1.1" + Token = "aa:bb:cc:dd:ee:ff" + Strategy = "MAC" + Meta = { + "field1" = "value1" + "field2" = "value2" + } + }` + +func TestAccDrpReservation_basic(t *testing.T) { + reservation := models.Reservation{ + Addr: net.ParseIP("1.1.1.1"), + Token: "aa:bb:cc:dd:ee:ff", + Strategy: "MAC", + Meta: map[string]string{"field1": "value1", "field2": "value2"}, + } + + reservation.Fill() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccDrpPreCheck(t) }, + Providers: testAccDrpProviders, + CheckDestroy: testAccDrpCheckReservationDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDrpReservation_basic, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckReservationExists(t, "drp_reservation.foo", &reservation), + ), + }, + }, + }) +} + +var testAccDrpReservation_change_1 = ` + resource "drp_reservation" "foo" { + Addr = "1.1.1.1" + Token = "aa:bb:cc:dd:ee:ff" + Strategy = "MAC" + NextServer = "1.2.3.4" + Options = [ + { Code = 30, Value = "fred" }, + { Code = 3, Value = "1.1.1.1" } + ] + }` + +var testAccDrpReservation_change_2 = ` + resource "drp_reservation" "foo" { + Addr = "1.1.1.1" + Token = "aa:bb:cc:dd:ee:ff" + Strategy = "MAC" + NextServer = "1.2.3.5" + Options = [ + { Code = 33, Value = "fred" }, + { Code = 4, Value = "1.2.1.1" } + ] + }` + +func TestAccDrpReservation_change(t *testing.T) { + reservation1 := models.Reservation{ + Addr: net.ParseIP("1.1.1.1"), + Token: "aa:bb:cc:dd:ee:ff", + Strategy: "MAC", + NextServer: net.ParseIP("1.2.3.4"), + Options: []models.DhcpOption{ + models.DhcpOption{Code: 30, Value: "fred"}, + models.DhcpOption{Code: 3, Value: "1.1.1.1"}, + }, + } + reservation1.Fill() + reservation2 := models.Reservation{ + Addr: net.ParseIP("1.1.1.1"), + Token: "aa:bb:cc:dd:ee:ff", + Strategy: "MAC", + NextServer: net.ParseIP("1.2.3.5"), + Options: []models.DhcpOption{ + models.DhcpOption{Code: 33, Value: "fred"}, + models.DhcpOption{Code: 4, Value: "1.2.1.1"}, + }, + } + reservation2.Fill() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccDrpPreCheck(t) }, + Providers: testAccDrpProviders, + CheckDestroy: testAccDrpCheckReservationDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDrpReservation_change_1, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckReservationExists(t, "drp_reservation.foo", &reservation1), + ), + }, + resource.TestStep{ + Config: testAccDrpReservation_change_2, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckReservationExists(t, "drp_reservation.foo", &reservation2), + ), + }, + }, + }) +} + +func testAccDrpCheckReservationDestroy(s *terraform.State) error { + config := testAccDrpProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "drp_reservation" { + continue + } + + if _, err := config.session.GetModel("reservations", rs.Primary.ID); err == nil { + return fmt.Errorf("Reservation still exists") + } + } + + return nil +} + +func testAccDrpCheckReservationExists(t *testing.T, n string, reservation *models.Reservation) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccDrpProvider.Meta().(*Config) + + obj, err := config.session.GetModel("reservations", rs.Primary.ID) + if err != nil { + return err + } + found := obj.(*models.Reservation) + found.ClearValidation() + + if found.Key() != rs.Primary.ID { + return fmt.Errorf("Reservation not found: %s %s", found.Key(), rs.Primary.ID) + } + + if err := diffObjects(reservation, found, "Reservation"); err != nil { + return err + } + return nil + } +} diff --git a/drp/resource_drp_stage_test.go b/drp/resource_drp_stage_test.go new file mode 100644 index 0000000..536fee0 --- /dev/null +++ b/drp/resource_drp_stage_test.go @@ -0,0 +1,173 @@ +package drp + +import ( + "fmt" + "testing" + + "github.com/digitalrebar/provision/models" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +var testAccDrpStage_basic = ` + resource "drp_stage" "foo" { + Name = "foo" + Meta = { + "field1" = "value1" + "field2" = "value2" + } + }` + +func TestAccDrpStage_basic(t *testing.T) { + stage := models.Stage{Name: "foo", + Meta: map[string]string{"field1": "value1", "field2": "value2"}, + } + stage.Fill() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccDrpPreCheck(t) }, + Providers: testAccDrpProviders, + CheckDestroy: testAccDrpCheckStageDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDrpStage_basic, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckStageExists(t, "drp_stage.foo", &stage), + ), + }, + }, + }) +} + +var testAccDrpStage_change_1 = ` + resource "drp_stage" "foo" { + Name = "foo" + Description = "I am a stage" + RequiredParams = [ "p1", "p2" ] + OptionalParams = [ "p3", "p4" ] + Templates = [ + { Name = "t1", Path = "fred1", Contents = "temp1"}, + { Name = "t2", Path = "fred2", Contents = "actual stuff"} + ] + BootEnv = "local" + Tasks = [ "t1", "t2" ] + Profiles = [ "p1" ] + Reboot = true + RunnerWait = true + }` + +var testAccDrpStage_change_2 = ` + resource "drp_stage" "foo" { + Name = "foo" + Description = "I am a stage again" + RequiredParams = [ "p3", "p4" ] + OptionalParams = [ "p1", "p2" ] + Templates = [ + { Name = "t3", Path = "jill1", Contents = "temp2"}, + { Name = "t4", Path = "jill2", Contents = "really actual stuff"} + ] + Tasks = [ "t3", "t4", "t5" ] + Profiles = [ "p2", "p3" ] + Reboot = false + RunnerWait = false + }` + +func TestAccDrpStage_change(t *testing.T) { + stage1 := models.Stage{ + Name: "foo", + Description: "I am a stage", + RequiredParams: []string{"p1", "p2"}, + OptionalParams: []string{"p3", "p4"}, + Templates: []models.TemplateInfo{ + {Name: "t1", Path: "fred1", Contents: "temp1"}, + {Name: "t2", Path: "fred2", Contents: "actual stuff"}, + }, + BootEnv: "local", + Tasks: []string{"t1", "t2"}, + Profiles: []string{"p1"}, + Reboot: true, + RunnerWait: true, + } + stage1.Fill() + stage2 := models.Stage{ + Name: "foo", + Description: "I am a stage again", + RequiredParams: []string{"p3", "p4"}, + OptionalParams: []string{"p1", "p2"}, + Templates: []models.TemplateInfo{ + {Name: "t3", Path: "jill1", Contents: "temp2"}, + {Name: "t4", Path: "jill2", Contents: "really actual stuff"}, + }, + BootEnv: "local", + Tasks: []string{"t3", "t4", "t5"}, + Profiles: []string{"p2", "p3"}, + } + stage2.Fill() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccDrpPreCheck(t) }, + Providers: testAccDrpProviders, + CheckDestroy: testAccDrpCheckStageDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDrpStage_change_1, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckStageExists(t, "drp_stage.foo", &stage1), + ), + }, + { + Config: testAccDrpStage_change_2, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckStageExists(t, "drp_stage.foo", &stage2), + ), + }, + }, + }) +} + +func testAccDrpCheckStageDestroy(s *terraform.State) error { + config := testAccDrpProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "drp_stage" { + continue + } + + if _, err := config.session.GetModel("stages", rs.Primary.ID); err == nil { + return fmt.Errorf("Stage still exists") + } + } + + return nil +} + +func testAccDrpCheckStageExists(t *testing.T, n string, stage *models.Stage) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccDrpProvider.Meta().(*Config) + + obj, err := config.session.GetModel("stages", rs.Primary.ID) + if err != nil { + return err + } + found := obj.(*models.Stage) + found.ClearValidation() + + if found.Name != rs.Primary.ID { + return fmt.Errorf("Stage not found") + } + + if err := diffObjects(stage, found, "Stage"); err != nil { + return err + } + return nil + } +} diff --git a/drp/resource_drp_subnet_test.go b/drp/resource_drp_subnet_test.go new file mode 100644 index 0000000..70d49e9 --- /dev/null +++ b/drp/resource_drp_subnet_test.go @@ -0,0 +1,207 @@ +package drp + +import ( + "fmt" + "net" + "testing" + + "github.com/digitalrebar/provision/models" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +var testAccDrpSubnet_basic = ` + resource "drp_subnet" "foo" { + Name = "foo" + Subnet = "1.1.1.0/24" + Strategy = "MAC" + ActiveStart = "1.1.1.4" + ActiveEnd = "1.1.1.9" + ActiveLeaseTime = 120 + ReservedLeaseTime = 14400 + Options = [] + Meta = { + "field1" = "value1" + "field2" = "value2" + } + }` + +func TestAccDrpSubnet_basic(t *testing.T) { + subnet := models.Subnet{Name: "foo", + ActiveLeaseTime: 120, + ReservedLeaseTime: 14400, + Subnet: "1.1.1.0/24", + ActiveStart: net.ParseIP("1.1.1.4"), + ActiveEnd: net.ParseIP("1.1.1.9"), + Strategy: "MAC", + Meta: map[string]string{"field1": "value1", "field2": "value2"}, + Pickers: []string{"hint", "nextFree", "mostExpired"}, + Options: []*models.DhcpOption{ + &models.DhcpOption{Code: 1, Value: "255.255.255.0"}, + &models.DhcpOption{Code: 28, Value: "1.1.1.255"}, + }, + } + + subnet.Fill() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccDrpPreCheck(t) }, + Providers: testAccDrpProviders, + CheckDestroy: testAccDrpCheckSubnetDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDrpSubnet_basic, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckSubnetExists(t, "drp_subnet.foo", &subnet), + ), + }, + }, + }) +} + +var testAccDrpSubnet_change_1 = ` + resource "drp_subnet" "foo" { + Name = "foo" + Enabled = true + Proxy = true + Subnet = "1.1.1.0/24" + NextServer = "1.1.1.1" + ActiveStart = "1.1.1.4" + ActiveEnd = "1.1.1.9" + ActiveLeaseTime = 120 + ReservedLeaseTime = 14400 + OnlyReservations = true + Strategy = "MAC" + Options = [ + { Code = 30, Value = "fred" }, + { Code = 3, Value = "1.1.1.1" } + ] + Pickers = [ "none" ] + }` + +var testAccDrpSubnet_change_2 = ` + resource "drp_subnet" "foo" { + Name = "foo" + Enabled = false + Proxy = false + Subnet = "1.1.1.1/24" + NextServer = "1.1.1.2" + ActiveStart = "1.1.1.5" + ActiveEnd = "1.1.1.10" + ActiveLeaseTime = 121 + ReservedLeaseTime = 14401 + OnlyReservations = false + Strategy = "MAC" + Options = [ + { Code = 33, Value = "fred" }, + { Code = 4, Value = "1.2.1.1" } + ] + Pickers = [ "hint", "none" ] + }` + +func TestAccDrpSubnet_change(t *testing.T) { + subnet1 := models.Subnet{Name: "foo", + ActiveLeaseTime: 120, + Enabled: true, + Proxy: true, + OnlyReservations: true, + ReservedLeaseTime: 14400, + Subnet: "1.1.1.0/24", + NextServer: net.ParseIP("1.1.1.1"), + ActiveStart: net.ParseIP("1.1.1.4"), + ActiveEnd: net.ParseIP("1.1.1.9"), + Strategy: "MAC", + Pickers: []string{"none"}, + Options: []*models.DhcpOption{ + &models.DhcpOption{Code: 30, Value: "fred"}, + &models.DhcpOption{Code: 3, Value: "1.1.1.1"}, + &models.DhcpOption{Code: 1, Value: "255.255.255.0"}, + &models.DhcpOption{Code: 28, Value: "1.1.1.255"}, + }, + } + subnet1.Fill() + subnet2 := models.Subnet{Name: "foo", + ActiveLeaseTime: 121, + ReservedLeaseTime: 14401, + Subnet: "1.1.1.1/24", + NextServer: net.ParseIP("1.1.1.2"), + ActiveStart: net.ParseIP("1.1.1.5"), + ActiveEnd: net.ParseIP("1.1.1.10"), + Strategy: "MAC", + Pickers: []string{"hint", "none"}, + Options: []*models.DhcpOption{ + &models.DhcpOption{Code: 33, Value: "fred"}, + &models.DhcpOption{Code: 4, Value: "1.2.1.1"}, + &models.DhcpOption{Code: 1, Value: "255.255.255.0"}, + &models.DhcpOption{Code: 28, Value: "1.1.1.255"}, + }, + } + subnet2.Fill() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccDrpPreCheck(t) }, + Providers: testAccDrpProviders, + CheckDestroy: testAccDrpCheckSubnetDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDrpSubnet_change_1, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckSubnetExists(t, "drp_subnet.foo", &subnet1), + ), + }, + resource.TestStep{ + Config: testAccDrpSubnet_change_2, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckSubnetExists(t, "drp_subnet.foo", &subnet2), + ), + }, + }, + }) +} + +func testAccDrpCheckSubnetDestroy(s *terraform.State) error { + config := testAccDrpProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "drp_subnet" { + continue + } + + if _, err := config.session.GetModel("subnets", rs.Primary.ID); err == nil { + return fmt.Errorf("Subnet still exists") + } + } + + return nil +} + +func testAccDrpCheckSubnetExists(t *testing.T, n string, subnet *models.Subnet) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccDrpProvider.Meta().(*Config) + + obj, err := config.session.GetModel("subnets", rs.Primary.ID) + if err != nil { + return err + } + found := obj.(*models.Subnet) + found.ClearValidation() + + if found.Name != rs.Primary.ID { + return fmt.Errorf("Subnet not found") + } + + if err := diffObjects(subnet, found, "Subnet"); err != nil { + return err + } + return nil + } +} diff --git a/drp/resource_drp_task_test.go b/drp/resource_drp_task_test.go new file mode 100644 index 0000000..9ee36f8 --- /dev/null +++ b/drp/resource_drp_task_test.go @@ -0,0 +1,163 @@ +package drp + +import ( + "fmt" + "testing" + + "github.com/digitalrebar/provision/models" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +var testAccDrpTask_basic = ` + resource "drp_task" "foo" { + Name = "foo" + Meta = { + "feature-flags" = "original-exit-codes" + "field1" = "value1" + "field2" = "value2" + } + }` + +func TestAccDrpTask_basic(t *testing.T) { + task := models.Task{Name: "foo", + Meta: map[string]string{"field1": "value1", "field2": "value2", "feature-flags": "original-exit-codes"}, + } + task.Fill() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccDrpPreCheck(t) }, + Providers: testAccDrpProviders, + CheckDestroy: testAccDrpCheckTaskDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDrpTask_basic, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckTaskExists(t, "drp_task.foo", &task), + ), + }, + }, + }) +} + +var testAccDrpTask_change_1 = ` + resource "drp_task" "foo" { + Name = "foo" + Description = "I am a task" + Documentation = "I am docs" + RequiredParams = [ "p1", "p2" ] + OptionalParams = [ "p3", "p4" ] + Templates = [ + { Name = "t1", Path = "fred1", Contents = "temp1"}, + { Name = "t2", Path = "fred2", Contents = "actual stuff"} + ] + }` + +var testAccDrpTask_change_2 = ` + resource "drp_task" "foo" { + Name = "foo" + Description = "I am a task again" + Documentation = "I am docs more so" + RequiredParams = [ "p3", "p4" ] + OptionalParams = [ "p1", "p2" ] + Templates = [ + { Name = "t3", Path = "jill1", Contents = "temp2"}, + { Name = "t4", Path = "jill2", Contents = "really actual stuff"} + ] + }` + +func TestAccDrpTask_change(t *testing.T) { + task1 := models.Task{ + Name: "foo", + Description: "I am a task", + Documentation: "I am docs", + Meta: map[string]string{"feature-flags": "original-exit-codes"}, + RequiredParams: []string{"p1", "p2"}, + OptionalParams: []string{"p3", "p4"}, + Templates: []models.TemplateInfo{ + {Name: "t1", Path: "fred1", Contents: "temp1"}, + {Name: "t2", Path: "fred2", Contents: "actual stuff"}, + }, + } + task1.Fill() + task2 := models.Task{ + Name: "foo", + Description: "I am a task again", + Documentation: "I am docs more so", + Meta: map[string]string{"feature-flags": "original-exit-codes"}, + RequiredParams: []string{"p3", "p4"}, + OptionalParams: []string{"p1", "p2"}, + Templates: []models.TemplateInfo{ + {Name: "t3", Path: "jill1", Contents: "temp2"}, + {Name: "t4", Path: "jill2", Contents: "really actual stuff"}, + }, + } + task2.Fill() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccDrpPreCheck(t) }, + Providers: testAccDrpProviders, + CheckDestroy: testAccDrpCheckTaskDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDrpTask_change_1, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckTaskExists(t, "drp_task.foo", &task1), + ), + }, + { + Config: testAccDrpTask_change_2, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckTaskExists(t, "drp_task.foo", &task2), + ), + }, + }, + }) +} + +func testAccDrpCheckTaskDestroy(s *terraform.State) error { + config := testAccDrpProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "drp_task" { + continue + } + + if _, err := config.session.GetModel("tasks", rs.Primary.ID); err == nil { + return fmt.Errorf("Task still exists") + } + } + + return nil +} + +func testAccDrpCheckTaskExists(t *testing.T, n string, task *models.Task) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccDrpProvider.Meta().(*Config) + + obj, err := config.session.GetModel("tasks", rs.Primary.ID) + if err != nil { + return err + } + found := obj.(*models.Task) + found.ClearValidation() + + if found.Name != rs.Primary.ID { + return fmt.Errorf("Task not found") + } + + if err := diffObjects(task, found, "Task"); err != nil { + return err + } + return nil + } +} diff --git a/drp/resource_drp_template_test.go b/drp/resource_drp_template_test.go new file mode 100644 index 0000000..c987114 --- /dev/null +++ b/drp/resource_drp_template_test.go @@ -0,0 +1,128 @@ +package drp + +import ( + "fmt" + "testing" + + "github.com/digitalrebar/provision/models" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +var testAccDrpTemplate_basic = ` + resource "drp_template" "foo" { + ID = "foo" + Meta = { + "field1" = "value1" + "field2" = "value2" + } + }` + +func TestAccDrpTemplate_basic(t *testing.T) { + template := models.Template{ID: "foo", + Meta: map[string]string{"field1": "value1", "field2": "value2"}, + } + template.Fill() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccDrpPreCheck(t) }, + Providers: testAccDrpProviders, + CheckDestroy: testAccDrpCheckTemplateDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDrpTemplate_basic, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckTemplateExists(t, "drp_template.foo", &template), + ), + }, + }, + }) +} + +var testAccDrpTemplate_change_1 = ` + resource "drp_template" "foo" { + ID = "foo" + Description = "I am a template" + Contents = "base content" + }` + +var testAccDrpTemplate_change_2 = ` + resource "drp_template" "foo" { + ID = "foo" + Description = "I am a template again" + Contents = "{{ .Env.OS }}" + }` + +func TestAccDrpTemplate_change(t *testing.T) { + template1 := models.Template{ID: "foo", Description: "I am a template", Contents: "base content"} + template1.Fill() + template2 := models.Template{ID: "foo", Description: "I am a template again", Contents: "{{ .Env.OS }}"} + template2.Fill() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccDrpPreCheck(t) }, + Providers: testAccDrpProviders, + CheckDestroy: testAccDrpCheckTemplateDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDrpTemplate_change_1, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckTemplateExists(t, "drp_template.foo", &template1), + ), + }, + resource.TestStep{ + Config: testAccDrpTemplate_change_2, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckTemplateExists(t, "drp_template.foo", &template2), + ), + }, + }, + }) +} + +func testAccDrpCheckTemplateDestroy(s *terraform.State) error { + config := testAccDrpProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "drp_template" { + continue + } + + if _, err := config.session.GetModel("templates", rs.Primary.ID); err == nil { + return fmt.Errorf("Template still exists") + } + } + + return nil +} + +func testAccDrpCheckTemplateExists(t *testing.T, n string, template *models.Template) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccDrpProvider.Meta().(*Config) + + obj, err := config.session.GetModel("templates", rs.Primary.ID) + if err != nil { + return err + } + found := obj.(*models.Template) + found.ClearValidation() + + if found.ID != rs.Primary.ID { + return fmt.Errorf("Template not found") + } + + if err := diffObjects(template, found, "Template"); err != nil { + return err + } + return nil + } +} diff --git a/drp/resource_drp_user_test.go b/drp/resource_drp_user_test.go new file mode 100644 index 0000000..b217ae1 --- /dev/null +++ b/drp/resource_drp_user_test.go @@ -0,0 +1,135 @@ +package drp + +import ( + "fmt" + "testing" + + "github.com/digitalrebar/provision/models" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +var testAccDrpUser_basic = ` + resource "drp_user" "foo" { + Name = "foo" + Meta = { + "field1" = "value1" + "field2" = "value2" + } + }` + +func TestAccDrpUser_basic(t *testing.T) { + user := models.User{Name: "foo", + Meta: map[string]string{"field1": "value1", "field2": "value2"}, + } + user.Fill() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccDrpPreCheck(t) }, + Providers: testAccDrpProviders, + CheckDestroy: testAccDrpCheckUserDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDrpUser_basic, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckUserExists(t, "drp_user.foo", &user), + ), + }, + }, + }) +} + +var testAccDrpUser_change_1 = ` + resource "drp_user" "foo" { + Name = "foo" + Secret = "I am a user" + }` + +var testAccDrpUser_change_2 = ` + resource "drp_user" "foo" { + Name = "foo" + Secret = "I am a user again" + }` + +func TestAccDrpUser_change(t *testing.T) { + user1 := models.User{Name: "foo", Secret: "I am a user"} + user1.Fill() + user2 := models.User{Name: "foo", Secret: "I am a user again"} + user2.Fill() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccDrpPreCheck(t) }, + Providers: testAccDrpProviders, + CheckDestroy: testAccDrpCheckUserDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDrpUser_change_1, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckUserExists(t, "drp_user.foo", &user1), + ), + }, + resource.TestStep{ + Config: testAccDrpUser_change_2, + Check: resource.ComposeTestCheckFunc( + testAccDrpCheckUserExists(t, "drp_user.foo", &user2), + ), + }, + }, + }) +} + +// XXX: One day worry about setting user's passwords. + +func testAccDrpCheckUserDestroy(s *terraform.State) error { + config := testAccDrpProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "drp_user" { + continue + } + + if _, err := config.session.GetModel("users", rs.Primary.ID); err == nil { + return fmt.Errorf("User still exists") + } + } + + return nil +} + +func testAccDrpCheckUserExists(t *testing.T, n string, user *models.User) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccDrpProvider.Meta().(*Config) + + obj, err := config.session.GetModel("users", rs.Primary.ID) + if err != nil { + return err + } + found := obj.(*models.User) + found.ClearValidation() + + if found.Name != rs.Primary.ID { + return fmt.Errorf("User not found") + } + + // Secret is unset, it should be set to something + if user.Secret == "" { + if found.Secret != "" { + user.Secret = found.Secret + } + } + + if err := diffObjects(user, found, "User"); err != nil { + return err + } + return nil + } +} diff --git a/drp/utils.go b/drp/utils.go new file mode 100644 index 0000000..9c55455 --- /dev/null +++ b/drp/utils.go @@ -0,0 +1,517 @@ +package drp + +import ( + "encoding/json" + "fmt" + "log" + "net" + "reflect" + "strings" + "time" + + "github.com/VictorLowther/jsonpatch2/utils" + "github.com/digitalrebar/provision/models" + "github.com/go-test/deep" + "github.com/hashicorp/terraform/helper/schema" + "github.com/pborman/uuid" +) + +func buildSchemaListFromObject(m interface{}) *schema.Schema { + r := &schema.Resource{ + Schema: buildSchemaFromObject(m), + } + return &schema.Schema{ + Type: schema.TypeList, + Elem: r, + Optional: true, + Computed: true, + } +} + +func buildSchemaFromObject(m interface{}) map[string]*schema.Schema { + sm := map[string]*schema.Schema{} + + val := reflect.ValueOf(m).Elem() + + for i := 0; i < val.NumField(); i++ { + typeField := val.Type().Field(i) + tag := typeField.Tag + + // Skip non-exported fields + if typeField.PkgPath != "" { + continue + } + + // Skip the access and validation fields + if typeField.Name == "Access" || typeField.Name == "Validation" { + continue + } + + // Skip the Profile - deprecated fields + if typeField.Name == "Profile" { + continue + } + + fieldName := typeField.Name + // Provider is reserved Terraform name + if fieldName == "Provider" { + fieldName = "PluginProvider" + } + + // Meta is a constant map of strings (but shows up as a type of Meta - fix it) + if fieldName == "Meta" { + sm["Meta"] = &schema.Schema{ + Type: schema.TypeMap, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + Computed: true, + } + + continue + } + + // + // This is a cluster. Terraform doesn't generic interface{} + // basically, interface{} and map[string]interface{} + // + // Will try some things. + // + if fieldName == "Params" { + sm["Params"] = &schema.Schema{ + Type: schema.TypeMap, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + Computed: true, + } + continue + } + if fieldName == "Schema" { + sm["Schema"] = &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + } + continue + } + + if strings.HasPrefix(typeField.Type.String(), "[]") { + listType := typeField.Type.String()[2:] + + switch listType { + case "string": + sm[fieldName] = &schema.Schema{ + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + Computed: true, + } + case "models.DhcpOption", "*models.DhcpOption": + sm[fieldName] = buildSchemaListFromObject(&models.DhcpOption{}) + + case "models.TemplateInfo": + sm[fieldName] = buildSchemaListFromObject(&models.TemplateInfo{}) + case "uint8": + sm[fieldName] = &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + } + default: + fmt.Printf("[DEBUG] UNKNOWN List Field Name: %s (%s),\t Tag Value: %s\n", + fieldName, typeField.Type, + tag.Get("tag_name")) + } + continue + } + + switch typeField.Type.String() { + case "models.OsInfo": + // Singleton struct - encode as a list for now. + sm[fieldName] = buildSchemaListFromObject(&models.OsInfo{}) + case "string", "net.IP", "uuid.UUID", "time.Time": + sm[fieldName] = &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + } + case "bool": + sm[fieldName] = &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Computed: true, + } + case "int", "int32", "uint8": + sm[fieldName] = &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Computed: true, + } + default: + fmt.Printf("[DEBUG] UNKNOWN Base Field Name: %s (%s),\t Tag Value: %s\n", + fieldName, typeField.Type, + tag.Get("tag_name")) + } + } + + return sm +} + +func resourceGeneric(pref string) *schema.Resource { + log.Printf("[DEBUG] [resourceGeneric] Initializing data structure: %s\n", pref) + m, _ := models.New(pref) + return buildSchema(m) +} + +func buildSchema(m models.Model) *schema.Resource { + r := &schema.Resource{ + Create: createDefaultCreateFunction(m), + Read: createDefaultReadFunction(m), + Update: createDefaultUpdateFunction(m), + Delete: createDefaultDeleteFunction(m), + Exists: createDefaultExistsFunction(m), + Schema: buildSchemaFromObject(m), + } + + return r +} + +func updateResourceData(m models.Model, d *schema.ResourceData) error { + val := reflect.ValueOf(m).Elem() + + for i := 0; i < val.NumField(); i++ { + valueField := val.Field(i) + typeField := val.Type().Field(i) + tag := typeField.Tag + + // Skip the access and validation fields + if typeField.Name == "Access" || typeField.Name == "Validation" { + continue + } + + // Skip the Profile - deprecated fields + if typeField.Name == "Profile" { + continue + } + + fieldName := typeField.Name + // Provider is reserved Terraform name + if fieldName == "Provider" { + fieldName = "PluginProvider" + } + + // Meta is a constant map of strings (but shows up as a type of Meta - fix it) + if fieldName == "Meta" { + d.Set("Meta", valueField.Interface()) + continue + } + + // + // This is a cluster. Terraform doesn't generic interface{} + // basically, interface{} and map[string]interface{} + // + // Will try some things. + // + if fieldName == "Params" { + answer := map[string]string{} + + drpAnswer := valueField.Interface().(map[string]interface{}) + for k, v := range drpAnswer { + b, e := json.Marshal(v) + if e != nil { + return e + } + if s, ok := v.(string); ok { + answer[k] = s + } else { + answer[k] = string(b) + } + } + d.Set("Params", answer) + continue + } + if fieldName == "Schema" { + b, e := json.Marshal(valueField.Interface()) + if e != nil { + return e + } + d.Set("Schema", string(b)) + continue + } + + if strings.HasPrefix(typeField.Type.String(), "[]") { + listType := typeField.Type.String()[2:] + + switch listType { + case "string", "*models.DhcpOption", "models.DhcpOption", "models.TemplateInfo": + d.Set(fieldName, valueField.Interface()) + case "uint8": + d.Set(fieldName, fmt.Sprintf("%s", valueField.Interface())) + default: + log.Printf("[DEBUG] UNKNOWN Field Name: %s (%s),\t Field Value: %v,\t Tag Value: %s\n", + fieldName, typeField.Type, + valueField.Interface(), tag.Get("tag_name")) + } + continue + } + + switch typeField.Type.String() { + case "models.OsInfo": + d.Set(fieldName, []models.OsInfo{valueField.Interface().(models.OsInfo)}) + case "string", "net.IP", "uuid.UUID", "time.Time": + d.Set(fieldName, fmt.Sprintf("%s", valueField.Interface())) + case "bool": + d.Set(fieldName, valueField.Interface()) + case "int", "int32", "uint8": + d.Set(fieldName, valueField.Interface()) + default: + log.Printf("[DEBUG] UNKNOWN Field Name: %s (%s),\t Field Value: %v,\t Tag Value: %s\n", + fieldName, typeField.Type, + valueField.Interface(), tag.Get("tag_name")) + } + } + return nil +} + +func buildModel(m models.Model, d *schema.ResourceData) (models.Model, error) { + new := models.Clone(m) + + val := reflect.ValueOf(new).Elem() + for i := 0; i < val.NumField(); i++ { + valueField := val.Field(i) + typeField := val.Type().Field(i) + tag := typeField.Tag + + // Skip the access and validation fields + if typeField.Name == "Access" || typeField.Name == "Validation" { + continue + } + + // Skip the Profile - deprecated fields + if typeField.Name == "Profile" { + continue + } + + fieldName := typeField.Name + // Provider is reserved Terraform name + if fieldName == "Provider" { + fieldName = "PluginProvider" + } + + if !d.HasChange(fieldName) { + continue + } + + // Meta is a constant map of strings (but shows up as a type of Meta - fix it) + if fieldName == "Meta" { + valueField.Set(reflect.MakeMap(typeField.Type)) + ms := d.Get("Meta").(map[string]interface{}) + for k, v := range ms { + valueField.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(v)) + } + continue + } + + // + // This is a cluster. Terraform doesn't generic interface{} + // basically, interface{} and map[string]interface{} + // + // Will try some things. + // + if fieldName == "Params" { + answer := d.Get("Params").(map[string]interface{}) + + valueField.Set(reflect.MakeMap(typeField.Type)) + + for k, v := range answer { + s := v.(string) + + var i interface{} + if e := json.Unmarshal([]byte(s), &i); e != nil { + i = s + } + + valueField.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(i)) + } + continue + } + if fieldName == "Schema" { + s := d.Get("Schema").(string) + var i interface{} + if e := json.Unmarshal([]byte(s), &i); e != nil { + return nil, e + } + valueField.Set(reflect.ValueOf(i)) + continue + } + + if strings.HasPrefix(typeField.Type.String(), "[]") { + listType := typeField.Type.String()[2:] + subType := typeField.Type.Elem() + + switch listType { + case "string", "models.TemplateInfo", + "models.DhcpOption", "*models.DhcpOption": + + data := d.Get(fieldName).([]interface{}) + v := reflect.MakeSlice(typeField.Type, 0, len(data)) + for _, s := range data { + no := reflect.New(subType).Interface() + if err := utils.Remarshal(s, no); err != nil { + return nil, err + } + v = reflect.Append(v, reflect.Indirect(reflect.ValueOf(no))) + } + valueField.Set(v) + + case "uint8": + fmt.Printf("[DEBUG] list of %s not support for push to DRP\n", listType) + default: + fmt.Printf("[DEBUG] UNKNOWN Field Name: %s (%s),\t Field Value: %v,\t Tag Value: %s\n", + fieldName, typeField.Type, + valueField.Interface(), tag.Get("tag_name")) + } + continue + } + + switch typeField.Type.String() { + case "models.OsInfo": + data := d.Get(fieldName).([]interface{}) + for _, s := range data { + no := models.OsInfo{} + if err := utils.Remarshal(s, &no); err != nil { + return nil, err + } + valueField.Set(reflect.ValueOf(no)) + break + } + case "string": + valueField.SetString(d.Get(fieldName).(string)) + case "net.IP": + ip := net.ParseIP(d.Get(fieldName).(string)) + valueField.Set(reflect.ValueOf(ip)) + case "uuid.UUID": + uu := uuid.Parse(d.Get(fieldName).(string)) + valueField.Set(reflect.ValueOf(uu)) + case "time.Time": + if t, e := time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", + d.Get(fieldName).(string)); e != nil { + return nil, e + } else { + valueField.Set(reflect.ValueOf(t)) + } + case "bool": + valueField.SetBool(d.Get(fieldName).(bool)) + case "int", "int32", "uint8": + valueField.SetInt(int64(d.Get(fieldName).(int))) + default: + fmt.Printf("[DEBUG] UNKNOWN Field Name: %s (%s),\t Field Value: %v,\t Tag Value: %s\n", + fieldName, typeField.Type, + valueField.Interface(), tag.Get("tag_name")) + } + } + return new, nil +} + +func createDefaultCreateFunction(m models.Model) func(*schema.ResourceData, interface{}) error { + return func(d *schema.ResourceData, meta interface{}) error { + cc := meta.(*Config) + log.Printf("[DEBUG] [resource%sCreate] creating\n", m.Prefix()) + + new, err := buildModel(m, d) + if err != nil { + return err + } + + answer, err := cc.session.GetModel(new.Prefix(), new.Key()) + if err == nil { + d.SetId(answer.Key()) + ro, ok := answer.(models.Accessor) + if !ok || ro.IsReadOnly() { + return updateResourceData(answer, d) + } + return createDefaultUpdateFunction(m)(d, meta) + } + + err = cc.session.CreateModel(new) + if err != nil { + return err + } + + d.SetId(new.Key()) + + return createDefaultReadFunction(m)(d, meta) + } +} + +func createDefaultReadFunction(m models.Model) func(*schema.ResourceData, interface{}) error { + return func(d *schema.ResourceData, meta interface{}) error { + cc := meta.(*Config) + log.Printf("[DEBUG] [resource%sRead] reading %s\n", m.Prefix(), d.Id()) + + answer, err := cc.session.GetModel(m.Prefix(), d.Id()) + if err != nil { + return err + } + + return updateResourceData(answer, d) + } +} + +func createDefaultUpdateFunction(m models.Model) func(*schema.ResourceData, interface{}) error { + return func(d *schema.ResourceData, meta interface{}) error { + cc := meta.(*Config) + log.Printf("[DEBUG] [resource%sUpdate] updating %s\n", m.Prefix(), d.Id()) + + base, err := cc.session.GetModel(m.Prefix(), d.Id()) + if err != nil { + return err + } + + mods, err := buildModel(base, d) + if err != nil { + return err + } + + err = cc.session.Req().PatchTo(base, mods).Params("force", "true").Do(&mods) + if err != nil { + return err + } + return updateResourceData(mods, d) + } +} + +func createDefaultDeleteFunction(m models.Model) func(*schema.ResourceData, interface{}) error { + return func(d *schema.ResourceData, meta interface{}) error { + cc := meta.(*Config) + log.Printf("[DEBUG] [resource%sDelete] deleting %s\n", m.Prefix(), d.Id()) + _, err := cc.session.DeleteModel(m.Prefix(), d.Id()) + return err + } +} + +func createDefaultExistsFunction(m models.Model) func(*schema.ResourceData, interface{}) (bool, error) { + return func(d *schema.ResourceData, meta interface{}) (bool, error) { + cc := meta.(*Config) + log.Printf("[DEBUG] [resource%sExists] testing %s\n", m.Prefix(), d.Id()) + return cc.session.ExistsModel(m.Prefix(), d.Id()) + } +} + +func diffObjects(exp, fnd interface{}, t string) error { + b1, _ := json.MarshalIndent(exp, "", " ") + b2, _ := json.MarshalIndent(fnd, "", " ") + if string(b1) != string(b2) { + return fmt.Errorf("json diff: %s: %v\n%v\n", t, string(b1), string(b2)) + + } + if diff := deep.Equal(exp, fnd); diff != nil { + return fmt.Errorf("%s doesn't match: %v", t, diff) + } + return nil +} diff --git a/glide.lock b/glide.lock index 77ee834..fb0bde2 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: e21916d47a83d4cf0d935955b779e6cff883a95793a3fa1f6dc3ad07b72e00d8 -updated: 2017-11-14T16:51:15.430648-06:00 +hash: 3e5186e8b7b5da2b89790346056808ffa79815712ddf914829c72069276830ef +updated: 2017-12-15T16:44:11.156225-06:00 imports: - name: github.com/apparentlymart/go-cidr version: 2bd8b58cf4275aeb086ade613de226773e29e853 @@ -43,16 +43,38 @@ imports: version: a476722483882dd40b8111f0eb64e1d7f43f56e4 subpackages: - spew +- name: github.com/dgrijalva/jwt-go + version: a539ee1a749a2b895533f979515ac7e6e0f5b650 +- name: github.com/digitalrebar/pinger + version: 4b06d02345cad02a772ff21f13e1ef3551c943d4 - name: github.com/digitalrebar/provision - version: 3af10535d31b6367778d34446d3092ed543aa059 + version: 8ed9e1a8a51f65013be437e17ec67ab8d2f53afa subpackages: + - api + - backend + - backend/index + - frontend + - midlayer - models + - server - name: github.com/digitalrebar/store version: f88fa4df9c617dc0bbd7b4361fd882dabbc44ad6 - name: github.com/elithrar/simple-scrypt version: 2325946f714c95de4a6088202c402fbdfa64163b - name: github.com/ghodss/yaml version: 0ca9ea5df5451ffdf184b4428c902747c2c11cd7 +- name: github.com/gin-contrib/cors + version: 88488351b0df049b2d3d65cab583c6bed894abb2 +- name: github.com/gin-contrib/location + version: 3e26b9c1187f298ae3669e41d1b696d256113f66 +- name: github.com/gin-contrib/sse + version: 22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae +- name: github.com/gin-gonic/gin + version: 8902826696c1dad024e1c8ba4f903aa61a25c839 + subpackages: + - binding + - json + - render - name: github.com/go-ini/ini version: c787282c39ac1fc618827141a1f762240def08a3 - name: github.com/golang/protobuf @@ -63,6 +85,8 @@ imports: - ptypes/any - ptypes/duration - ptypes/timestamp +- name: github.com/gorilla/websocket + version: a69d9f6de432e2c6b296a947d8a5ee88f68522cf - name: github.com/hashicorp/consul version: a0b974620c29ec9372fa0583b4f1d0f0315062c8 subpackages: @@ -131,10 +155,14 @@ imports: version: d1caa6c97c9fc1cc9e83bbe34d0603f9ff0ce8bd - name: github.com/jmespath/go-jmespath version: bd40a432e4c76585ef6b72d3fd96fb9b6dc7b68d +- name: github.com/json-iterator/go + version: 2dc0031b26575ddf5dab09ab7795105a05575473 - name: github.com/krolaw/dhcp4 version: 4de04cc59d6e4223583ec8117238e4d57e8b176d subpackages: - conn +- name: github.com/mattn/go-isatty + version: fc9e8d8ef48496124e79ae0df75490096eccf6fe - name: github.com/mitchellh/copystructure version: d23ffcb85de31694d6ccaa23ccb4a03e55c1303f - name: github.com/mitchellh/go-homedir @@ -149,8 +177,16 @@ imports: version: 63d60e9d0dbc60cf9164e6510889b0db6683d98c - name: github.com/pborman/uuid version: e790cca94e6cc75c7064b1332e63811d4aae1a53 +- name: github.com/pin/tftp + version: d715325c1fc1f1a22a8272a60dc6783a5ab093ba + subpackages: + - netascii - name: github.com/satori/go.uuid version: 5bf94b69c6b68ee1b541973bb8e1144db23a194b +- name: github.com/ugorji/go + version: 8c0409fcbb70099c748d71f714529204975f6c3f + subpackages: + - codec - name: github.com/ulikunitz/xz version: 0c6b41e72360850ca4f98dc341fd999726ea007f subpackages: @@ -158,9 +194,15 @@ imports: - internal/xlog - lzma - name: github.com/VictorLowther/jsonpatch2 - version: c7b50f49dce1ebe7c627bf0a92c87b633a45bf43 + version: 728f2fee2a186b5764d9c9a8fa904279a22b1960 subpackages: - utils +- name: github.com/vishvananda/netlink + version: f67b75edbf5e3bb7dfe70bb788610693a71be3d1 + subpackages: + - nl +- name: github.com/vishvananda/netns + version: be1fbeda19366dea804f00efff2dd73a1642fdcc - name: github.com/xeipuuv/gojsonpointer version: 6fe8760cad3569743d51ddbb243b26f8456742dc - name: github.com/xeipuuv/gojsonreference @@ -179,12 +221,15 @@ imports: - context - http2 - http2/hpack + - icmp - idna - internal/iana - internal/socket - internal/timeseries - ipv4 + - ipv6 - lex/httplex + - route - trace - name: golang.org/x/sys version: 2d6f6f883a06fc0d5f4b14a81e4c28705ea64c15 @@ -222,6 +267,14 @@ imports: - status - tap - transport +- name: gopkg.in/go-playground/validator.v8 + version: 5f1438d3fca68893a817e4a66806cea46a9e4ebf +- name: gopkg.in/olahol/melody.v1 + version: d521390733761fe1db13de575c253afd5c743085 - name: gopkg.in/yaml.v2 version: eb3733d160e74a9c7e442f435eb3bea458e1d19f -testImports: [] +testImports: +- name: github.com/go-test/deep + version: 9898238679c264cfb10411539f14a0553dc8b295 +- name: github.com/jessevdk/go-flags + version: 6cf8f02b4ae8ba723ddc64dcfd403e530c06d927 diff --git a/glide.yaml b/glide.yaml index 3fbc45c..795b914 100644 --- a/glide.yaml +++ b/glide.yaml @@ -1,7 +1,6 @@ package: github.com/rackn/terraform-provider-drp import: - package: github.com/hashicorp/terraform - version: v0.9.4 subpackages: - plugin - terraform @@ -14,3 +13,4 @@ import: - package: github.com/VictorLowther/jsonpatch2 subpackages: - utils +-package: github.com/jessevdk/go-flags diff --git a/main.go b/main.go index d34159e..84e7d8c 100644 --- a/main.go +++ b/main.go @@ -2,9 +2,9 @@ package main import ( "github.com/hashicorp/terraform/plugin" - "github.com/rackn/terraform-provider-drp/provider" + "github.com/rackn/terraform-provider-drp/drp" ) func main() { - plugin.Serve(&plugin.ServeOpts{ProviderFunc: provider.Provider}) + plugin.Serve(&plugin.ServeOpts{ProviderFunc: drp.Provider}) } diff --git a/provider/machine.go b/provider/machine.go deleted file mode 100644 index 0b433de..0000000 --- a/provider/machine.go +++ /dev/null @@ -1,327 +0,0 @@ -package provider - -import ( - "fmt" - "log" - "net/url" - "strings" - "time" - - "github.com/hashicorp/terraform/helper/resource" - "github.com/hashicorp/terraform/helper/schema" - "github.com/rackn/terraform-provider-drp/client" -) - -// This function doesn't really *create* a new machine but, power an already registered -// machine. -func resourceDRPMachineCreate(d *schema.ResourceData, meta interface{}) error { - log.Println("[DEBUG] [resourceDRPMachineCreate] Launching new drp_machine") - cc := meta.(*client.Client) - - constraints, err := parseConstraints(d) - if err != nil { - log.Println("[ERROR] [resourceDRPMachineCreate] Unable to parse constraints.") - return err - } - - machineObj, err := cc.AllocateMachine(constraints) - if err != nil { - log.Println("[ERROR] [resourceDRPMachineCreate] Unable to allocate machine: %v", err) - return err - } - - cBootEnv := machineObj.BootEnv - - // Update the machine to request position - err = cc.UpdateMachine(machineObj, constraints) - if err != nil { - log.Println("[ERROR] [resourceDRPMachineCreate] Unable to initialize machine: %v", err) - if err2 := cc.ReleaseMachine(machineObj.UUID()); err2 != nil { - log.Println("[ERROR] [resourceDRPMachineCreate] Unable to release machine: %v", err2) - } - return err - } - - if err := cc.MachineDo(machineObj.UUID(), "nextbootpxe", url.Values{}); err != nil { - log.Printf("[ERROR] [resourceDRPMachineCreate] Unable to mark the machine for pxe next boot: %s\n", machineObj.UUID()) - if err2 := cc.ReleaseMachine(machineObj.UUID()); err2 != nil { - log.Println("[ERROR] [resourceDRPMachineCreate] Unable to release machine: %v", err2) - } - return err - } - - // Power on and then cycle, if needed - powerAction := "poweron" - if err := cc.MachineDo(machineObj.UUID(), powerAction, url.Values{}); err != nil { - log.Printf("[ERROR] [resourceDRPMachineCreate] Unable to power cycleup machine: %s\n", machineObj.UUID()) - if err2 := cc.ReleaseMachine(machineObj.UUID()); err2 != nil { - log.Println("[ERROR] [resourceDRPMachineCreate] Unable to release machine: %v", err2) - } - return err - } - - machineObj, err = cc.GetMachine(machineObj.UUID()) - if err != nil { - log.Println("[ERROR] [resourceDRPMachineCreate] Unable to release machine: %v", err) - return err - } - if machineObj.BootEnv != cBootEnv { - powerAction := "powercycle" - - if err := cc.MachineDo(machineObj.UUID(), powerAction, url.Values{}); err != nil { - log.Printf("[ERROR] [resourceDRPMachineCreate] Unable to power cycleup machine: %s\n", machineObj.UUID()) - if err2 := cc.ReleaseMachine(machineObj.UUID()); err2 != nil { - log.Println("[ERROR] [resourceDRPMachineCreate] Unable to release machine: %v", err2) - } - return err - } - } - - log.Printf("[DEBUG] [resourceDRPMachineCreate] Waiting for machine (%s) to become active\n", machineObj.UUID()) - stateConf := &resource.StateChangeConf{ - Pending: []string{"9:"}, - Target: []string{"6:"}, - Refresh: cc.GetMachineStatus(machineObj.UUID()), - Timeout: 25 * time.Minute, - Delay: 10 * time.Second, - MinTimeout: 3 * time.Second, - } - - if _, err := stateConf.WaitForState(); err != nil { - if err2 := cc.ReleaseMachine(machineObj.UUID()); err2 != nil { - log.Println("[ERROR] [resourceDRPMachineCreate] Unable to release machine: %v", err2) - } - return fmt.Errorf( - "[ERROR] [resourceDRPMachineCreate] Error waiting for machine (%s) to become deployed: %s", - machineObj.UUID(), err) - } - - d.SetId(machineObj.UUID()) - return nil -} - -func resourceDRPMachineExists(d *schema.ResourceData, meta interface{}) (bool, error) { - cc := meta.(*client.Client) - log.Printf("[DEBUG] Exists machine (%s) information.\n", d.Id()) - return cc.ExistsMachine(d.Id()) -} - -func resourceDRPMachineRead(d *schema.ResourceData, meta interface{}) error { - log.Printf("[DEBUG] Reading machine (%s) information.\n", d.Id()) - return nil -} - -func resourceDRPMachineUpdate(d *schema.ResourceData, meta interface{}) error { - cc := meta.(*client.Client) - log.Printf("[DEBUG] [resourceDRPMachineUpdate] Modifying machine %s\n", d.Id()) - - constraints, err := parseConstraints(d) - if err != nil { - log.Println("[ERROR] [resourceDRPMachineUpdate] Unable to parse constraints.") - return err - } - - machineObj, err := cc.GetMachine(d.Id()) - if err != nil { - log.Println("[ERROR] [resourceDRPMachineUpdate] Failed to get machine: %v", err) - return err - } - - // Update the machine to request position - err = cc.UpdateMachine(machineObj, constraints) - if err != nil { - log.Println("[ERROR] [resourceDRPMachineUpdate] Unable to initialize machine: %v", err) - return err - } - - log.Printf("[DEBUG] Done Modifying machine %s", d.Id()) - return nil -} - -// This function doesn't really *delete* a drp managed machine but releases (read, turns off) the machine. -func resourceDRPMachineDelete(d *schema.ResourceData, meta interface{}) error { - cc := meta.(*client.Client) - log.Printf("[DEBUG] Deleting machine %s\n", d.Id()) - - machineObj, err := cc.GetMachine(d.Id()) - if err != nil { - log.Println("[ERROR] [resourceDRPMachineDelete] Failed to get machine: %v", err) - return err - } - - retVal := url.Values{} - if machineObj.Stage != "" { - retVal["stage"] = []string{"discover"} - } else { - retVal["bootenv"] = []string{"sledgehammer"} - } - - // Update the machine to request position - err = cc.UpdateMachine(machineObj, retVal) - if err != nil { - log.Println("[ERROR] [resourceDRPMachineDelete] Unable to reset machine: %v", err) - return err - } - - if err := cc.ReleaseMachine(d.Id()); err != nil { - return err - } - - if err := cc.MachineDo(machineObj.UUID(), "nextbootpxe", url.Values{}); err != nil { - log.Printf("[ERROR] [resourceDRPMachineRelease] Unable to mark the machine for pxe next boot: %s\n", machineObj.UUID()) - } - if err := cc.MachineDo(machineObj.UUID(), "powercycle", url.Values{}); err != nil { - log.Printf("[ERROR] [resourceDRPMachineRelease] Unable to power cycle machine: %s\n", machineObj.UUID()) - } - - log.Printf("[DEBUG] [resourceDRPMachineDelete] Machine (%s) released", d.Id()) - - d.SetId("") - - return nil -} - -var stringParams = []string{ - "name", - "bootenv", - "stage", - "owner", - "description", -} - -func parseConstraints(d *schema.ResourceData) (url.Values, error) { - log.Println("[DEBUG] [parseConstraints] Parsing any existing DRP constraints") - retVal := url.Values{} - - for _, s := range stringParams { - sval, set := d.GetOk(s) - if set { - log.Printf("[DEBUG] [parseConstraints] setting %s to %+v", s, sval) - retVal[s] = strings.Fields(sval.(string)) - } - } - - udval, set := d.GetOk("userdata") - if set { - retVal["userdata"] = []string{udval.(string)} - } - - retVal["profiles"] = []string{} - aval, set := d.GetOk("profiles") - if set { - for _, p := range aval.([]interface{}) { - retVal["profiles"] = append(retVal["profiles"], p.(string)) - } - } - - retVal["parameters"] = []string{} - pval, set := d.GetOk("parameters") - if set { - for _, o := range pval.([]interface{}) { - v := o.(map[string]interface{}) - name := v["name"] - value := v["value"].(string) - retVal["parameters"] = append(retVal["parameters"], fmt.Sprintf("%s=%s", name, value)) - } - } - - retVal["filters"] = []string{} - pval, set = d.GetOk("filters") - if set { - for _, o := range pval.([]interface{}) { - v := o.(map[string]interface{}) - name := v["name"] - value := v["value"].(string) - retVal["filters"] = append(retVal["filters"], fmt.Sprintf("%s=%s", name, value)) - } - } - - return retVal, nil -} - -func resourceDRPMachine() *schema.Resource { - log.Println("[DEBUG] [resourceDRPMachine] Initializing data structure") - return &schema.Resource{ - Create: resourceDRPMachineCreate, - Read: resourceDRPMachineRead, - Update: resourceDRPMachineUpdate, - Delete: resourceDRPMachineDelete, - Exists: resourceDRPMachineExists, - - SchemaVersion: 1, - - Schema: map[string]*schema.Schema{ - "bootenv": { - Type: schema.TypeString, - Optional: true, - }, - - "stage": { - Type: schema.TypeString, - Optional: true, - }, - - "owner": { - Type: schema.TypeString, - Optional: true, - }, - - "name": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - }, - - "description": { - Type: schema.TypeString, - Optional: true, - }, - - "userdata": { - Type: schema.TypeString, - Optional: true, - }, - - "filters": { - Type: schema.TypeList, - Optional: true, - ForceNew: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Optional: true, - }, - "value": { - Type: schema.TypeString, - Optional: true, - }, - }, - }, - }, - - "profiles": { - Type: schema.TypeList, - Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, - }, - - "parameters": { - Type: schema.TypeList, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Optional: true, - }, - "value": { - Type: schema.TypeString, - Optional: true, - }, - }, - }, - }, - }, - } -} diff --git a/provider/provider.go b/provider/provider.go deleted file mode 100644 index 28eeec3..0000000 --- a/provider/provider.go +++ /dev/null @@ -1,76 +0,0 @@ -package provider - -import ( - "fmt" - "log" - - "github.com/hashicorp/terraform/helper/schema" - "github.com/hashicorp/terraform/terraform" - "github.com/rackn/terraform-provider-drp/client" -) - -/* - * Enable terraform to use us as a provider. Fill out the - * appropriate functions and information about this plugin. - */ -func Provider() terraform.ResourceProvider { - log.Println("[DEBUG] Initializing the DRP provider") - return &schema.Provider{ - Schema: map[string]*schema.Schema{ - "api_key": { - Type: schema.TypeString, - Optional: true, - Description: "The api key for API operations", - }, - "api_user": { - Type: schema.TypeString, - Optional: true, - Description: "The api user for API operations", - }, - "api_password": { - Type: schema.TypeString, - Optional: true, - Description: "The api password for API operations", - }, - "api_url": { - Type: schema.TypeString, - Required: true, - Description: "The DRP server URL. ie: https://1.2.3.4:8092", - }, - }, - - ResourcesMap: map[string]*schema.Resource{ - "drp_machine": resourceDRPMachine(), - }, - - ConfigureFunc: providerConfigure, - } -} - -/* - * The config method that terraform uses to pass information about configuration - * to the plugin. - */ -func providerConfigure(d *schema.ResourceData) (interface{}, error) { - log.Println("[DEBUG] Configuring the DRP provider") - cc := client.Client{ - APIURL: d.Get("api_url").(string), - } - - if key := d.Get("api_key"); key != nil { - cc.APIKey = key.(string) - } - if user := d.Get("api_user"); user != nil { - cc.APIUser = user.(string) - cc.APIPassword = d.Get("api_password").(string) - } - - if cc.APIKey == "" && cc.APIUser == "" { - return nil, fmt.Errorf("drp provider requires either user or token ids") - } - if cc.APIUser != "" && cc.APIPassword == "" { - return nil, fmt.Errorf("drp provider requires a password for the specified user") - } - - return cc.Client() -} diff --git a/scripts/changelog-links.sh b/scripts/changelog-links.sh new file mode 100755 index 0000000..ced5fa4 --- /dev/null +++ b/scripts/changelog-links.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# This script rewrites [GH-nnnn]-style references in the CHANGELOG.md file to +# be Markdown links to the given github issues. +# +# This is run during releases so that the issue references in all of the +# released items are presented as clickable links, but we can just use the +# easy [GH-nnnn] shorthand for quickly adding items to the "Unrelease" section +# while merging things between releases. + +set -e + +if [[ ! -f CHANGELOG.md ]]; then + echo "ERROR: CHANGELOG.md not found in pwd." + echo "Please run this from the root of the terraform provider repository" + exit 1 +fi + +if [[ `uname` == "Darwin" ]]; then + echo "Using BSD sed" + SED="sed -i.bak -E -e" +else + echo "Using GNU sed" + SED="sed -i.bak -r -e" +fi + +PROVIDER_URL="https:\/\/github.com\/terraform-providers\/terraform-provider-cobbler\/issues" + +$SED "s/GH-([0-9]+)/\[#\1\]\($PROVIDER_URL\/\1\)/g" -e 's/\[\[#(.+)([0-9])\)]$/(\[#\1\2))/g' CHANGELOG.md + +rm CHANGELOG.md.bak diff --git a/scripts/errcheck.sh b/scripts/errcheck.sh new file mode 100755 index 0000000..15464f5 --- /dev/null +++ b/scripts/errcheck.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# Check gofmt +echo "==> Checking for unchecked errors..." + +if ! which errcheck > /dev/null; then + echo "==> Installing errcheck..." + go get -u github.com/kisielk/errcheck +fi + +err_files=$(errcheck -ignoretests \ + -ignore 'github.com/hashicorp/terraform/helper/schema:Set' \ + -ignore 'bytes:.*' \ + -ignore 'io:Close|Write' \ + $(go list ./...| grep -v /vendor/)) + +if [[ -n ${err_files} ]]; then + echo 'Unchecked errors found in the following places:' + echo "${err_files}" + echo "Please handle returned errors. You can check directly with \`make errcheck\`" + exit 1 +fi + +exit 0 diff --git a/scripts/gofmtcheck.sh b/scripts/gofmtcheck.sh new file mode 100755 index 0000000..1c05581 --- /dev/null +++ b/scripts/gofmtcheck.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +# Check gofmt +echo "==> Checking that code complies with gofmt requirements..." +gofmt_files=$(gofmt -l `find . -name '*.go' | grep -v vendor`) +if [[ -n ${gofmt_files} ]]; then + echo 'gofmt needs running on the following files:' + echo "${gofmt_files}" + echo "You can use the command: \`make fmt\` to reformat code." + exit 1 +fi + +exit 0 diff --git a/scripts/gogetcookie.sh b/scripts/gogetcookie.sh new file mode 100755 index 0000000..26c63a6 --- /dev/null +++ b/scripts/gogetcookie.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +touch ~/.gitcookies +chmod 0600 ~/.gitcookies + +git config --global http.cookiefile ~/.gitcookies + +tr , \\t <<\__END__ >>~/.gitcookies +.googlesource.com,TRUE,/,TRUE,2147483647,o,git-paul.hashicorp.com=1/z7s05EYPudQ9qoe6dMVfmAVwgZopEkZBb1a2mA5QtHE +__END__ diff --git a/terraform/._Description.meta b/terraform/._Description.meta deleted file mode 100644 index 4c540cc..0000000 --- a/terraform/._Description.meta +++ /dev/null @@ -1 +0,0 @@ -Content data for using the DRP Terraform Provider diff --git a/terraform/._Name.meta b/terraform/._Name.meta deleted file mode 100644 index 56fe0d6..0000000 --- a/terraform/._Name.meta +++ /dev/null @@ -1 +0,0 @@ -terraform \ No newline at end of file diff --git a/terraform/._Source.meta b/terraform/._Source.meta deleted file mode 100644 index c87ebd3..0000000 --- a/terraform/._Source.meta +++ /dev/null @@ -1 +0,0 @@ -RackN \ No newline at end of file diff --git a/terraform/params/terraform.allocated.json b/terraform/params/terraform.allocated.json deleted file mode 100644 index 707bfad..0000000 --- a/terraform/params/terraform.allocated.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "Name": "terraform/allocated", - "Description": "Indicates if this machine has been allocated by terraform", - "Schema": { - "type": "boolean" - }, - "Meta": { - "icon": "hand rock", - "color": "yellow", - "title": "RackN Content" - } -} diff --git a/terraform/params/terraform.managed.json b/terraform/params/terraform.managed.json deleted file mode 100644 index 666e46a..0000000 --- a/terraform/params/terraform.managed.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "Name": "terraform/managed", - "Description": "Indicates if this machine can be managed by terraform", - "Schema": { - "type": "boolean" - }, - "Meta": { - "icon": "hand paper", - "color": "greenf", - "title": "RackN Content" - } -} diff --git a/terraform/params/terraform.owner.json b/terraform/params/terraform.owner.json deleted file mode 100644 index 85ebd8f..0000000 --- a/terraform/params/terraform.owner.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "Name": "terraform/owner", - "Description": "Helper parameter that indicates who allocated this machine", - "Schema": { - "type": "string" - }, - "Meta": { - "icon": "user circle outline", - "color": "blue", - "title": "RackN Content" - } -} diff --git a/terraform/params/terraform.poweroff.json b/terraform/params/terraform.poweroff.json deleted file mode 100644 index 0ee5d3a..0000000 --- a/terraform/params/terraform.poweroff.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "Name": "terraform/poweroff", - "Description": "Indicates whether the machine should be powered off while awaiting allocation by terraform", - "Schema": { - "type": "boolean" - }, - "Meta": { - "icon": "power cord", - "color": "black", - "title": "RackN Content" - } -} diff --git a/terraform/profiles/terraform-managed.json b/terraform/profiles/terraform-managed.json deleted file mode 100644 index 26539bc..0000000 --- a/terraform/profiles/terraform-managed.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "Name": "terraform-managed", - "Description": "Example base state for machines wanting to be managed by terraform", - "Params": { - "terraform/managed": true, - "terraform/allocated": false - }, - "Meta": { - "icon": "map outline", - "color": "blue", - "title": "RackN Content" - } -} diff --git a/terraform/stages/terraform-ready.yaml b/terraform/stages/terraform-ready.yaml deleted file mode 100644 index 0f1c4a1..0000000 --- a/terraform/stages/terraform-ready.yaml +++ /dev/null @@ -1,11 +0,0 @@ ---- -Name: "terraform-ready" -BootEnv: "sledgehammer" -Description: "marks a machine as managed by terraform and ready for allocation" -RunnerWait: true -Tasks: - - "terraform-enable" -Meta: - icon: "map outline" - color: "green" - title: "RackN Content" diff --git a/terraform/tasks/terraform-enable.yaml b/terraform/tasks/terraform-enable.yaml deleted file mode 100644 index bc05dd7..0000000 --- a/terraform/tasks/terraform-enable.yaml +++ /dev/null @@ -1,13 +0,0 @@ ---- -Name: "terraform-enable" -Description: "A task to enable terraform for the machine." -OptionalParameters: - - "terraform/poweroff" -Templates: - - ID: "terraform-enable.sh.tmpl" - Name: "The terraform enable script" - Path: "" -Meta: - icon: "map outline" - color: "blue" - title: "RackN Content" diff --git a/terraform/templates/terraform-enable.sh.tmpl b/terraform/templates/terraform-enable.sh.tmpl deleted file mode 100644 index 3efdfc7..0000000 --- a/terraform/templates/terraform-enable.sh.tmpl +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash -# -# This template marks a machine as terraform ready -# -# Required Parameters: -# Optional Parameters: -# -# Parameter YAML format: -# - -# This will contain a token appropriate for the path being -# used below. Either a create or update/show token -export RS_UUID="{{.Machine.UUID}}" -export RS_TOKEN="{{.GenerateToken}}" - -# Ubuntu Path is different than Centos Path - fix it. -export PATH=$PATH:/usr/bin:/usr/sbin:/bin:/sbin - -echo "Marking node for ready for terraform" - -if ! drpcli machines set $RS_UUID param terraform/allocated to false ; then - echo "Failed to mark node as not allocated" - exit 4 -fi - -if ! drpcli machines set $RS_UUID param terraform/managed to true ; then - echo "Failed to mark node as not allocated" - exit 4 -fi - -{{if .ParamExists "terraform/poweroff"}} -poweroff={{.Param "terraform/poweroff"}} -if [[ $poweroff == "true" ]] ; then - # Turn runnable off to indicate that we are sleepy. - drpcli machines update $RS_UUID '{ "Runnable": false }' - - # If we have a poweroff action, use it. - if drpcli machines action $RS_UUID poweroff ; then - drpcli machines runaction $RS_UUID poweroff - else - shutdown -P now - fi - # Wait for shutdown/poweroffs - sleep 30 -fi -{{end}} - -echo "Succesfully marked machine for terraform" -exit 0 - diff --git a/tools/package.sh b/tools/package.sh index 0a73ff2..cc38925 100755 --- a/tools/package.sh +++ b/tools/package.sh @@ -13,23 +13,10 @@ case $(uname -s) in exit 1;; esac - - -go get -u github.com/digitalrebar/provision/cmds/drbundler -PATH=$PATH:$GOPATH/bin - . tools/version.sh version="$Prepart$MajorV.$MinorV.$PatchV$Extra-$GITHASH" -for i in terraform ; do - cd $i - echo -n "$version" > ._Version.meta - cd .. - drbundler $i $i.yaml - $shasum $i.yaml > $i.sha256 -done - tmpdir="$(mktemp -d /tmp/rs-bundle-XXXXXXXX)" cp -a bin "$tmpdir" ( diff --git a/tools/publish.sh b/tools/publish.sh index 24eae8a..09891b9 100755 --- a/tools/publish.sh +++ b/tools/publish.sh @@ -2,11 +2,6 @@ set -e -[[ $GOPATH ]] || export GOPATH="$HOME/go" -fgrep -q "$GOPATH/bin" <<< "$PATH" || export PATH="$PATH:$GOPATH/bin" - -go get -u github.com/stevenroose/remarshal - . tools/version.sh version="$Prepart$MajorV.$MinorV.$PatchV$Extra-$GITHASH" @@ -14,9 +9,6 @@ TOKEN=R0cketSk8ts for i in terraform ; do echo "Publishing $i to cloud" CONTENT=$i - remarshal -i $CONTENT.yaml -o $CONTENT.json -if yaml -of json - curl -X PUT -T $CONTENT.json https://qww9e4paf1.execute-api.us-west-2.amazonaws.com/main/support/content/$CONTENT?token=$TOKEN - echo arches=("amd64") oses=("linux" "darwin" "windows") diff --git a/version.go b/version.go deleted file mode 100644 index b06d3e2..0000000 --- a/version.go +++ /dev/null @@ -1,10 +0,0 @@ -package main - -var RS_PREPART = "v" -var RS_MAJOR_VERSION = "3" -var RS_MINOR_VERSION = "0" -var RS_PATCH_VERSION = "2" -var RS_EXTRA = "-pre-alpha" -var GitHash = "NotSet" -var BuildStamp = "Not Set" -var RS_VERSION = RS_PREPART + RS_MAJOR_VERSION + "." + RS_MINOR_VERSION + "." + RS_PATCH_VERSION + RS_EXTRA + "-" + GitHash