Skip to content

Commit 6fb64fd

Browse files
committed
feat: introducing named profiles
``` As a Lacework CLI User with multiple Lacework Accounts, I would like to be able to configure my Lacework CLI with multiple profiles, So I can switch easily between accounts without having to reconfigure my configuration file. ``` This change is introducing named profiles and removing the parameter `--config` since we will hardcode the config to be in `$HOME/.lacework.toml`. Why? Since we can configure multiple profiles today, we do not need to pass in any other config location. Previous versions of the config are compatible to this change so there are no breaking changes. All environment variables previously defined continues to work in addition to the new environment variable `LW_PROFILE` that switches the profile to use. Closes #36 Signed-off-by: Salim Afiune Maya <[email protected]>
1 parent f67ca9a commit 6fb64fd

File tree

6 files changed

+334
-77
lines changed

6 files changed

+334
-77
lines changed

cli/cmd/api.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func runApiCommand(_ *cobra.Command, args []string) error {
8181
api.WithApiKeys(cli.KeyID, cli.Secret),
8282
)
8383
if err != nil {
84-
return errors.Wrap(err, "unable to generate Lacework api client")
84+
return errors.Wrap(err, "unable to generate api client")
8585
}
8686

8787
response := new(map[string]interface{})

cli/cmd/cli_state.go

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
//
2+
// Author:: Salim Afiune Maya (<[email protected]>)
3+
// Copyright:: Copyright 2020, Lacework Inc.
4+
// License:: Apache License, Version 2.0
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
19+
package cmd
20+
21+
import (
22+
"fmt"
23+
24+
prettyjson "github.com/hokaccha/go-prettyjson"
25+
"github.com/pkg/errors"
26+
"github.com/spf13/viper"
27+
"go.uber.org/zap"
28+
)
29+
30+
// cliState holds the state of the entire Lacework CLI
31+
type cliState struct {
32+
Profile string
33+
Account string
34+
KeyID string
35+
Secret string
36+
Token string
37+
LogLevel string
38+
39+
JsonF *prettyjson.Formatter
40+
Log *zap.SugaredLogger
41+
42+
profileDetails map[string]interface{}
43+
}
44+
45+
// NewDefaultState creates a new cliState with some defaults
46+
func NewDefaultState() cliState {
47+
return cliState{
48+
Profile: "default",
49+
JsonF: prettyjson.NewFormatter(),
50+
}
51+
}
52+
53+
// SetProfile sets the provided profile into the cliState and loads the entire
54+
// state of the Lacework CLI by calling 'LoadState()'
55+
func (c *cliState) SetProfile(profile string) error {
56+
if profile == "" {
57+
return errors.New("Specify a profile.")
58+
}
59+
60+
c.Profile = profile
61+
c.Log.Debugw("custom profile", "profile", profile)
62+
return c.LoadState()
63+
}
64+
65+
// LoadState loads the state of the cli in the following order, loads the
66+
// configured profile out from the viper loaded config, if the profile is
67+
// set to the default and it is not found, we assume that the user is running
68+
// the CLI with parameters or environment variables, so we proceed to load
69+
// those. Though, if the profile is NOT the default, we error out with some
70+
// breadcrumbs to help the user configure the CLI. After loading the profile,
71+
// this function verifies parameters and env variables coming from viper
72+
func (c *cliState) LoadState() error {
73+
c.profileDetails = viper.GetStringMap(c.Profile)
74+
if len(c.profileDetails) == 0 {
75+
if c.Profile != "default" {
76+
return fmt.Errorf(
77+
"The profile '%s' could not be found.\n\nTry running 'lacework configure --profile %s'.",
78+
c.Profile, c.Profile,
79+
)
80+
} else {
81+
c.Log.Debugw("unable to load state from config")
82+
c.loadStateFromViper()
83+
return nil
84+
}
85+
}
86+
87+
c.KeyID = c.extractValueString("api_key")
88+
c.Secret = c.extractValueString("api_secret")
89+
c.Account = c.extractValueString("account")
90+
91+
c.Log.Debugw("state loaded",
92+
"profile", c.Profile,
93+
"account", c.Account,
94+
"api_key", c.KeyID,
95+
"api_secret", c.Secret,
96+
)
97+
98+
c.loadStateFromViper()
99+
return nil
100+
}
101+
102+
// VerifySettings checks if the CLI state has the neccessary settings to run,
103+
// if not, it throws an error with breadcrumbs to help the user configure the CLI
104+
func (c *cliState) VerifySettings() error {
105+
if c.Profile == "" ||
106+
c.Account == "" ||
107+
c.Secret == "" ||
108+
c.KeyID == "" {
109+
return fmt.Errorf(
110+
"there is one or more settings missing.\n\nTry running 'lacework configure'.",
111+
)
112+
}
113+
114+
return nil
115+
}
116+
117+
// loadStateFromViper loads parameters and environment variables
118+
// coming from viper into the CLI state
119+
func (c *cliState) loadStateFromViper() {
120+
if v := viper.GetString("api_key"); v != "" {
121+
c.KeyID = v
122+
c.Log.Debugw("state updated", "api_key", c.KeyID)
123+
}
124+
125+
if v := viper.GetString("api_secret"); v != "" {
126+
c.Secret = v
127+
c.Log.Debugw("state updated", "api_secret", c.Secret)
128+
}
129+
130+
if v := viper.GetString("account"); v != "" {
131+
c.Account = v
132+
c.Log.Debugw("state updated", "account", c.Account)
133+
}
134+
}
135+
136+
func (c *cliState) extractValueString(key string) string {
137+
if val, ok := c.profileDetails[key]; ok {
138+
if str, ok := val.(string); ok {
139+
return str
140+
}
141+
c.Log.Warnw("config value type mismatch",
142+
"expected_type", "string",
143+
"file", viper.ConfigFileUsed(),
144+
"profile", c.Profile,
145+
"key", key,
146+
"value", val,
147+
)
148+
return ""
149+
}
150+
c.Log.Warnw("unable to find key from config",
151+
"file", viper.ConfigFileUsed(),
152+
"profile", c.Profile,
153+
"key", key,
154+
)
155+
return ""
156+
}

cli/cmd/configure.go

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,47 @@ import (
3232
"github.com/spf13/viper"
3333
)
3434

35-
// the representation of the ~/.lacework.toml
36-
type config struct {
37-
Account string `toml:"account"`
38-
ApiKey string `toml:"api_key"`
39-
ApiSecret string `toml:"api_secret"`
35+
// Profiles is the representation of the ~/.lacework.toml
36+
//
37+
// Example:
38+
//
39+
// [default]
40+
// account = "example"
41+
// api_key = "EXAMPLE_0123456789"
42+
// api_secret = "_0123456789"
43+
//
44+
// [dev]
45+
// account = "dev"
46+
// api_key = "DEV_0123456789"
47+
// api_secret = "_0123456789"
48+
type Profiles map[string]credsDetails
49+
50+
type credsDetails struct {
51+
Account string `toml:"account" json:"account"`
52+
ApiKey string `toml:"api_key" json:"api_key"`
53+
ApiSecret string `toml:"api_secret" json:"api_secret"`
4054
}
4155

4256
var (
4357
// configureCmd represents the configure command
4458
configureCmd = &cobra.Command{
4559
Use: "configure",
46-
Short: "Set up your Lacework cli",
60+
Short: "Configure the Lacework CLI",
4761
Args: cobra.NoArgs,
62+
Long: `
63+
Configure settings that the Lacework CLI uses to interact with the Lacework
64+
platform. These include your Lacework account, API access key and secret.
65+
66+
If this command is run with no arguments, the Lacework CLI will store all
67+
settings under the default profile. The information in the default profile
68+
is used any time you run a Lacework CLI command that doesn't explicitly
69+
specify a profile to use.
70+
71+
You can configure multiple profiles by using the --profile argument. If a
72+
config file does not exist (the default location is ~/.lacework.toml), the
73+
Lacework CLI will create it for you.`,
4874
RunE: func(_ *cobra.Command, _ []string) error {
75+
cli.Log.Debugw("configuring cli", "profile", cli.Profile)
4976
var (
5077
promptAccount = promptui.Prompt{
5178
Label: "Account",
@@ -95,21 +122,47 @@ var (
95122
}
96123

97124
var (
98-
c = config{account, apiKey, apiSecret}
125+
profiles = Profiles{}
99126
buf = new(bytes.Buffer)
100127
confPath = viper.ConfigFileUsed()
101128
)
102129

103-
if err := toml.NewEncoder(buf).Encode(c); err != nil {
104-
return err
105-
}
106-
107130
if confPath == "" {
108131
home, err := homedir.Dir()
109132
if err != nil {
110133
return err
111134
}
112135
confPath = path.Join(home, ".lacework.toml")
136+
cli.Log.Debugw("generating new config file",
137+
"path", confPath,
138+
)
139+
} else {
140+
if _, err := toml.DecodeFile(confPath, &profiles); err != nil {
141+
cli.Log.Debugw("unable to decode profiles from config, trying previous config",
142+
"path", confPath, "error", err,
143+
)
144+
145+
var oldcreds credsDetails
146+
if _, err2 := toml.DecodeFile(confPath, &oldcreds); err2 != nil {
147+
cli.Log.Debugw("unable to decode old config, no more options, exit",
148+
"error", err2,
149+
)
150+
return err
151+
}
152+
profiles["default"] = oldcreds
153+
}
154+
cli.Log.Debugw("profiles loaded from config, updating", "profiles", profiles)
155+
}
156+
157+
profiles[cli.Profile] = credsDetails{
158+
Account: account,
159+
ApiKey: apiKey,
160+
ApiSecret: apiSecret,
161+
}
162+
163+
cli.Log.Debugw("storing updated profiles", "profiles", profiles)
164+
if err := toml.NewEncoder(buf).Encode(profiles); err != nil {
165+
return err
113166
}
114167

115168
err = ioutil.WriteFile(confPath, buf.Bytes(), 0600)

cli/cmd/integration.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ var (
4545
api.WithApiKeys(cli.KeyID, cli.Secret),
4646
)
4747
if err != nil {
48-
return errors.Wrap(err, "unable to generate Lacework api client")
48+
return errors.Wrap(err, "unable to generate api client")
4949
}
5050

5151
integrations, err := lacework.Integrations.List()

0 commit comments

Comments
 (0)