Skip to content

Commit

Permalink
Merge pull request #6 from leg100/new-org-cmd
Browse files Browse the repository at this point in the history
Add CLI command: ots organizations new
  • Loading branch information
leg100 authored Jun 21, 2021
2 parents 06bebdf + a53d371 commit 373aabc
Show file tree
Hide file tree
Showing 16 changed files with 601 additions and 181 deletions.
26 changes: 6 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
39 changes: 39 additions & 0 deletions cmd/environment_variables.go
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))
}
76 changes: 76 additions & 0 deletions cmd/ots/client.go
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
}
44 changes: 44 additions & 0 deletions cmd/ots/client_test.go
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)
}
})
}
}
131 changes: 131 additions & 0 deletions cmd/ots/credentials_store.go
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
}
Loading

0 comments on commit 373aabc

Please sign in to comment.