-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6 from leg100/new-org-cmd
Add CLI command: ots organizations new
- Loading branch information
Showing
16 changed files
with
601 additions
and
181 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,40 +26,26 @@ These steps will get you started with running everything on your local system. Y | |
```bash | ||
sudo cp cert.crt /usr/local/share/ca-certificates | ||
sudo update-ca-certificates | ||
``` | ||
|
||
1. Run the OTS daemon: | ||
|
||
```bash | ||
./otsd -ssl -cert-file cert.crt -key-file key.pem | ||
./otsd --ssl --cert-file=cert.crt --key-file=key.pem | ||
``` | ||
|
||
The daemon runs in the foreground and can be left to run. | ||
|
||
1. In another terminal create an organization: | ||
|
||
```bash | ||
curl -H"Accept: application/vnd.api+json" https://localhost:8080/api/v2/organizations -d'{ | ||
"data": { | ||
"type": "organizations", | ||
"attributes": { | ||
"name": "mycorp", | ||
"email": "[email protected]" | ||
} | ||
} | ||
}' | ||
./ots organizations new mycorp [email protected] | ||
``` | ||
1. Enter some dummy credentials (this is necessary otherwise terraform will complain): | ||
|
||
1. Login to your OTS server (this merely adds some dummy credentials to `~/.terraform.d/credentials.tfrc.json`): | ||
|
||
```bash | ||
cat > ~/.terraform.d/credentials.tfrc.json <<EOF | ||
{ | ||
"credentials": { | ||
"localhost:8080": { | ||
"token": "dummy" | ||
} | ||
} | ||
} | ||
EOF | ||
./ots login | ||
``` | ||
|
||
1. Configure the terraform backend and define a resource: | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
package cmd | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"strings" | ||
|
||
"github.com/spf13/pflag" | ||
) | ||
|
||
const ( | ||
EnvironmentVariablePrefix = "OTS_" | ||
) | ||
|
||
// Each flag can also be set with an env variable whose name starts with `OTS_`. | ||
func SetFlagsFromEnvVariables(fs *pflag.FlagSet) { | ||
fs.VisitAll(func(f *pflag.Flag) { | ||
envVar := flagToEnvVarName(f) | ||
if val, present := os.LookupEnv(envVar); present { | ||
fs.Set(f.Name, val) | ||
} | ||
}) | ||
} | ||
|
||
// Unset env vars prefixed with `OTS_` | ||
func UnsetEtokVars() { | ||
for _, kv := range os.Environ() { | ||
parts := strings.Split(kv, "=") | ||
if strings.HasPrefix(parts[0], EnvironmentVariablePrefix) { | ||
if err := os.Unsetenv(parts[0]); err != nil { | ||
panic(err.Error()) | ||
} | ||
} | ||
} | ||
} | ||
|
||
func flagToEnvVarName(f *pflag.Flag) string { | ||
return fmt.Sprintf("%s%s", EnvironmentVariablePrefix, strings.Replace(strings.ToUpper(f.Name), "-", "_", -1)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"net/url" | ||
|
||
"github.com/hashicorp/go-tfe" | ||
) | ||
|
||
const ( | ||
DefaultAddress = "localhost:8080" | ||
) | ||
|
||
type ClientConfig interface { | ||
NewClient() (Client, error) | ||
} | ||
|
||
type clientConfig struct { | ||
tfe.Config | ||
} | ||
|
||
type Client interface { | ||
Organizations() tfe.Organizations | ||
} | ||
|
||
type client struct { | ||
*tfe.Client | ||
} | ||
|
||
func (c *client) Organizations() tfe.Organizations { | ||
return c.Client.Organizations | ||
} | ||
|
||
func (c *clientConfig) NewClient() (Client, error) { | ||
if err := c.sanitizeAddress(); err != nil { | ||
return nil, err | ||
} | ||
|
||
creds, err := NewCredentialsStore(&SystemDirectories{}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// If --token isn't set then load from DB | ||
if c.Token == "" { | ||
c.Token, err = creds.Load(c.Address) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
tfeClient, err := tfe.NewClient(&c.Config) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &client{Client: tfeClient}, nil | ||
} | ||
|
||
// Ensure address is in format https://<host>:<port> | ||
func (c *clientConfig) sanitizeAddress() error { | ||
u, err := url.ParseRequestURI(c.Address) | ||
if err != nil || u.Host == "" { | ||
u, er := url.ParseRequestURI("https://" + c.Address) | ||
if er != nil { | ||
return fmt.Errorf("could not parse hostname: %w", err) | ||
} | ||
c.Address = u.String() | ||
return nil | ||
} | ||
|
||
u.Scheme = "https" | ||
c.Address = u.String() | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package main | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/hashicorp/go-tfe" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestClientSanitizeAddress(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
address string | ||
want string | ||
}{ | ||
{ | ||
name: "add scheme", | ||
address: "localhost:8080", | ||
want: "https://localhost:8080", | ||
}, | ||
{ | ||
name: "already has scheme", | ||
address: "https://localhost:8080", | ||
want: "https://localhost:8080", | ||
}, | ||
{ | ||
name: "has wrong scheme", | ||
address: "http://localhost:8080", | ||
want: "https://localhost:8080", | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
cfg := clientConfig{ | ||
Config: tfe.Config{ | ||
Address: tt.address, | ||
}, | ||
} | ||
if assert.NoError(t, cfg.sanitizeAddress()) { | ||
assert.Equal(t, tt.want, cfg.Address) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
package main | ||
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"net/url" | ||
"os" | ||
"path/filepath" | ||
) | ||
|
||
const ( | ||
CredentialsPath = ".terraform.d/credentials.tfrc.json" | ||
) | ||
|
||
type CredentialsConfig struct { | ||
Credentials map[string]TokenConfig `json:"credentials"` | ||
} | ||
|
||
type TokenConfig struct { | ||
Token string `json:"token"` | ||
} | ||
|
||
// CredentialsStore is a JSON file in a user's home dir that stores tokens for | ||
// one or more TFE-type hosts | ||
type CredentialsStore string | ||
|
||
// NewCredentialsStore is a contructor for CredentialsStore | ||
func NewCredentialsStore(dirs Directories) (CredentialsStore, error) { | ||
// Construct full path to creds config | ||
home, err := dirs.UserHomeDir() | ||
if err != nil { | ||
return "", err | ||
} | ||
path := filepath.Join(home, CredentialsPath) | ||
|
||
return CredentialsStore(path), nil | ||
} | ||
|
||
// Load retrieves the token for hostname | ||
func (c CredentialsStore) Load(hostname string) (string, error) { | ||
hostname, err := c.sanitizeHostname(hostname) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
config, err := c.read() | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
tokenConfig, ok := config.Credentials[hostname] | ||
if !ok { | ||
return "", fmt.Errorf("credentials for %s not found in %s", hostname, c) | ||
} | ||
|
||
return tokenConfig.Token, nil | ||
} | ||
|
||
// Save saves the token for the given hostname to the store, overwriting any | ||
// existing tokens for the hostname. | ||
func (c CredentialsStore) Save(hostname, token string) error { | ||
hostname, err := c.sanitizeHostname(hostname) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
config, err := c.read() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
config.Credentials[hostname] = TokenConfig{ | ||
Token: token, | ||
} | ||
|
||
if err := c.write(config); err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (c CredentialsStore) read() (*CredentialsConfig, error) { | ||
// Construct credentials config obj | ||
config := CredentialsConfig{Credentials: make(map[string]TokenConfig)} | ||
|
||
// Read any existing file contents | ||
data, err := os.ReadFile(string(c)) | ||
if err == nil { | ||
if err := json.Unmarshal(data, &config); err != nil { | ||
return nil, err | ||
} | ||
} else if !errors.Is(err, os.ErrNotExist) { | ||
return nil, err | ||
} | ||
|
||
return &config, nil | ||
} | ||
|
||
func (c CredentialsStore) write(config *CredentialsConfig) error { | ||
data, err := json.MarshalIndent(&config, "", " ") | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// Ensure all parent directories of config file exist | ||
if err := os.MkdirAll(filepath.Dir(string(c)), 0775); err != nil { | ||
return err | ||
} | ||
|
||
if err := os.WriteFile(string(c), data, 0600); err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// Ensure hostname is in the format <host>:<port> | ||
func (c CredentialsStore) sanitizeHostname(hostname string) (string, error) { | ||
u, err := url.ParseRequestURI(hostname) | ||
if err != nil || u.Host == "" { | ||
u, er := url.ParseRequestURI("https://" + hostname) | ||
if er != nil { | ||
return "", fmt.Errorf("could not parse hostname: %w", err) | ||
} | ||
return u.Host, nil | ||
} | ||
|
||
return u.Host, nil | ||
} |
Oops, something went wrong.