diff --git a/cache/cache.go b/cache/cache.go index 3a45d7c..5cc8281 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -94,6 +94,24 @@ func (cc *CredentialCache) GetDefault() (*creds.RefreshableProvider, error) { return nil, errors.NoCredentialsFoundInCache } +func (cc *CredentialCache) DefaultLastUpdated() string { + c, err := cc.GetDefault() + if err != nil { + log.Debugf("cannot get last updated time of default creds: %v", err) + return "" + } + return c.LastRefreshed.UTC().Format("2006-01-02T15:04:05Z") +} + +func (cc *CredentialCache) DefaultArn() string { + c, err := cc.GetDefault() + if err != nil { + log.Debugf("cannot get arn of default creds: %v", err) + return "" + } + return c.RoleArn +} + func (cc *CredentialCache) get(slug string) (*creds.RefreshableProvider, bool) { cc.RLock() defer cc.RUnlock() diff --git a/cache/cache_test.go b/cache/cache_test.go index 8fffc66..a880d9b 100644 --- a/cache/cache_test.go +++ b/cache/cache_test.go @@ -212,6 +212,86 @@ func TestCredentialCache_SetDefault(t *testing.T) { } } +func TestCredentialCache_DefaultLastUpdated(t *testing.T) { + testCache := CredentialCache{ + RoleCredentials: map[string]*creds.RefreshableProvider{}, + } + testClient, err := creds.GetTestClient(creds.ConsolemeCredentialResponseType{ + Credentials: &creds.AwsCredentials{ + AccessKeyId: "a", + SecretAccessKey: "b", + SessionToken: "c", + Expiration: creds.Time(time.Unix(1, 0)), + RoleArn: "e", + }, + }) + if err != nil { + t.Errorf("test setup failure: %e", err) + } + err = testCache.SetDefault(testClient, "a", "b", make([]string, 0)) + if err != nil { + t.Errorf("test failure: %e", err) + } + timeFormat := "2006-01-02T15:04:05Z" + result := testCache.DefaultLastUpdated() + resultTime, err := time.Parse(timeFormat, result) + if err != nil { + t.Errorf("invalid time format returned: expected format %s, got result %s", timeFormat, result) + } + now := time.Now().Format(timeFormat) + timeDiff := time.Now().Sub(resultTime) + if timeDiff < 0*time.Second || timeDiff > 1*time.Second { + t.Errorf("last refreshed time more than 1 second different than current time: %s, current time %s, difference %v seconds", result, now, timeDiff) + } +} + +func TestCredentialCache_DefaultLastUpdated_NoDefault(t *testing.T) { + testCache := CredentialCache{ + RoleCredentials: map[string]*creds.RefreshableProvider{}, + } + result := testCache.DefaultLastUpdated() + if result != "" { + t.Errorf("wrong last updated returned: got %s, expected empty string", result) + } +} + +func TestCredentialCache_DefaultArn(t *testing.T) { + testCache := CredentialCache{ + RoleCredentials: map[string]*creds.RefreshableProvider{}, + } + testClient, err := creds.GetTestClient(creds.ConsolemeCredentialResponseType{ + Credentials: &creds.AwsCredentials{ + AccessKeyId: "a", + SecretAccessKey: "b", + SessionToken: "c", + Expiration: creds.Time(time.Unix(1, 0)), + RoleArn: "e", + }, + }) + if err != nil { + t.Errorf("test setup failure: %e", err) + } + err = testCache.SetDefault(testClient, "a", "b", make([]string, 0)) + if err != nil { + t.Errorf("test failure: %e", err) + } + expected := "e" + result := testCache.DefaultArn() + if result != expected { + t.Errorf("wrong arn returned: got %s, expected %s", result, expected) + } +} + +func TestCredentialCache_DefaultArn_NoDefault(t *testing.T) { + testCache := CredentialCache{ + RoleCredentials: map[string]*creds.RefreshableProvider{}, + } + result := testCache.DefaultArn() + if result != "" { + t.Errorf("wrong arn returned: got %s, expected empty string", result) + } +} + func TestCredentialCache_GetOrSet(t *testing.T) { cases := []struct { CacheContents map[string]*creds.RefreshableProvider diff --git a/cmd/completion.go b/cmd/completion.go index 729f3d4..b7b98df 100644 --- a/cmd/completion.go +++ b/cmd/completion.go @@ -24,9 +24,10 @@ import ( // completionCmd represents the completion command var completionCmd = &cobra.Command{ - Use: "completion [bash|zsh|fish|powershell]", - Short: "Generate completion script", - Long: `Generate shell completion script for Bash, Zsh, Fish, and Powershell.`, + Use: "completion [bash|zsh|fish|powershell]", + Short: completionShortHelp, + Long: completionLongHelp, + Hidden: true, } var bashCompletionCmd = &cobra.Command{ diff --git a/cmd/credential_process.go b/cmd/credential_process.go index 339c4f7..743c47a 100644 --- a/cmd/credential_process.go +++ b/cmd/credential_process.go @@ -29,26 +29,21 @@ import ( ) func init() { - CredentialProcessCmd.PersistentFlags().BoolVarP(&noIpRestrict, "no-ip", "n", false, "remove IP restrictions") - GenerateCredentialProcessCmd.PersistentFlags().StringVarP(&destinationConfig, "output", "o", getDefaultAwsConfigFile(), "output file for AWS config") + CredentialProcessCmd.PersistentFlags().BoolVarP(&generate, "generate", "g", false, "generate ~/.aws/config with credential process config") + CredentialProcessCmd.PersistentFlags().BoolVarP(&showAll, "all", "a", false, "include all roles (console and application)") + CredentialProcessCmd.PersistentFlags().StringVarP(&destinationConfig, "output", "o", getDefaultAwsConfigFile(), "output file for AWS config") rootCmd.AddCommand(CredentialProcessCmd) - rootCmd.AddCommand(GenerateCredentialProcessCmd) -} - -var GenerateCredentialProcessCmd = &cobra.Command{ - Use: "generate_credential_process_config", - Short: "Write all of your eligible roles as profiles in your AWS Config to source credentials from Weep", - RunE: runGenerateCredentialProcessConfig, } var CredentialProcessCmd = &cobra.Command{ Use: "credential_process [role_name]", - Short: "Retrieve credentials and writes them in credential_process format", - Args: cobra.ExactArgs(1), + Short: credentialProcessShortHelp, + Long: credentialProcessLongHelp, + Args: cobra.MaximumNArgs(1), RunE: runCredentialProcess, } -func writeConfigFile(roles []string) error { +func writeConfigFile(roles []string, destination string) error { var configINI *ini.File var err error @@ -78,16 +73,19 @@ func writeConfigFile(roles []string) error { return nil } -func runGenerateCredentialProcessConfig(cmd *cobra.Command, args []string) error { +func generateCredentialProcessConfig(destination string) error { + if destination == "" { + return fmt.Errorf("no destination provided") + } client, err := creds.GetClient() if err != nil { return err } - roles, err := client.Roles() + roles, err := client.Roles(showAll) if err != nil { return err } - err = writeConfigFile(roles) + err = writeConfigFile(roles, destination) if err != nil { return err } @@ -95,7 +93,10 @@ func runGenerateCredentialProcessConfig(cmd *cobra.Command, args []string) error } func runCredentialProcess(cmd *cobra.Command, args []string) error { - role = args[0] + if generate { + return generateCredentialProcessConfig(destination) + } + role := args[0] credentials, err := creds.GetCredentials(role, noIpRestrict, assumeRole) if err != nil { return err diff --git a/cmd/docs.go b/cmd/docs.go index 9ada021..c54f26c 100644 --- a/cmd/docs.go +++ b/cmd/docs.go @@ -23,7 +23,8 @@ import ( var docCommand = &cobra.Command{ Use: "docs", - Short: "Generate Markdown docs for CLI commands", + Short: docsShortHelp, + Long: docsLongHelp, Hidden: true, Run: func(cmd *cobra.Command, args []string) { err := doc.GenMarkdownTree(rootCmd, "./docs/") diff --git a/cmd/ecs_credential_provider.go b/cmd/ecs_credential_provider.go deleted file mode 100644 index 6715bb0..0000000 --- a/cmd/ecs_credential_provider.go +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2020 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package cmd - -import ( - "fmt" - "net" - "net/http" - - "github.com/spf13/viper" - - "github.com/gorilla/mux" - "github.com/netflix/weep/handlers" - "github.com/spf13/cobra" -) - -func init() { - ecsCredentialProvider.PersistentFlags().StringVarP(&ecsProviderListenAddr, "listen-address", "a", "127.0.0.1", "IP address for the ECS credential provider to listen on") - ecsCredentialProvider.PersistentFlags().IntVarP(&ecsProviderListenPort, "port", "p", viper.GetInt("server.ecs_credential_provider_port"), "port for the ECS credential provider service to listen on") - rootCmd.AddCommand(ecsCredentialProvider) -} - -var ecsCredentialProvider = &cobra.Command{ - Use: "ecs_credential_provider", - Short: "RunService a local ECS Credential Provider endpoint that serves and caches credentials for roles on demand", - RunE: runEcsMetadata, -} - -func runEcsMetadata(cmd *cobra.Command, args []string) error { - ipaddress := net.ParseIP(ecsProviderListenAddr) - - if ipaddress == nil { - return fmt.Errorf("invalid IP: %s", ecsProviderListenAddr) - } - - listenAddr := fmt.Sprintf("%s:%d", ipaddress, ecsProviderListenPort) - - router := mux.NewRouter() - router.HandleFunc("/healthcheck", handlers.HealthcheckHandler) - router.HandleFunc("/ecs/{role:.*}", handlers.CredentialServiceMiddleware(handlers.ECSMetadataServiceCredentialsHandler)) - router.HandleFunc("/{path:.*}", handlers.CredentialServiceMiddleware(handlers.CustomHandler)) - - go func() { - log.Info("Starting weep ECS meta-data service...") - log.Info("Server started on: ", listenAddr) - log.Info(http.ListenAndServe(listenAddr, router)) - }() - - // Check for interrupt signal and exit cleanly - <-shutdown - log.Print("Shutdown signal received, stopping server...") - return nil -} diff --git a/cmd/export.go b/cmd/export.go index 11609b8..f8b6beb 100644 --- a/cmd/export.go +++ b/cmd/export.go @@ -32,13 +32,14 @@ func init() { var exportCmd = &cobra.Command{ Use: "export [role_name]", - Short: "Retrieve credentials to be exported as environment variables", + Short: exportShortHelp, + Long: exportLongHelp, Args: cobra.ExactArgs(1), RunE: runExport, } func runExport(cmd *cobra.Command, args []string) error { - role = args[0] + role := args[0] creds, err := creds.GetCredentials(role, noIpRestrict, assumeRole) if err != nil { return err diff --git a/cmd/file.go b/cmd/file.go index 7e38141..2665b87 100644 --- a/cmd/file.go +++ b/cmd/file.go @@ -31,7 +31,6 @@ import ( ) func init() { - fileCmd.PersistentFlags().BoolVarP(&noIpRestrict, "no-ip", "n", false, "remove IP restrictions") fileCmd.PersistentFlags().StringVarP(&destination, "output", "o", getDefaultCredentialsFile(), "output file for credentials") fileCmd.PersistentFlags().StringVarP(&profileName, "profile", "p", "default", "profile name") fileCmd.PersistentFlags().BoolVarP(&force, "force", "f", false, "overwrite existing profile without prompting") @@ -41,13 +40,14 @@ func init() { var fileCmd = &cobra.Command{ Use: "file [role_name]", - Short: "Retrieve credentials and save them to a credentials file", + Short: fileShortHelp, + Long: fileLongHelp, Args: cobra.ExactArgs(1), RunE: runFile, } func runFile(cmd *cobra.Command, args []string) error { - role = args[0] + role := args[0] err := updateCredentialsFile(role, profileName, destination, noIpRestrict, assumeRole) if err != nil { return err diff --git a/cmd/list.go b/cmd/list.go index e853f19..e55ed51 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -24,12 +24,14 @@ import ( ) func init() { + listCmd.PersistentFlags().BoolVarP(&showAll, "all", "a", false, "include all roles (console and application)") rootCmd.AddCommand(listCmd) } var listCmd = &cobra.Command{ Use: "list", - Short: "List available roles", + Short: listShortHelp, + Long: listLongHelp, RunE: runList, } @@ -38,7 +40,7 @@ func runList(cmd *cobra.Command, args []string) error { if err != nil { return err } - roles, err := client.Roles() + roles, err := client.Roles(showAll) if err != nil { return err } diff --git a/cmd/metadata.go b/cmd/metadata.go index a85bafd..be70334 100644 --- a/cmd/metadata.go +++ b/cmd/metadata.go @@ -39,14 +39,15 @@ func init() { } var metadataCmd = &cobra.Command{ - Use: "metadata [role_name]", - Short: "RunService a local Instance Metadata Service (IMDS) endpoint that serves credentials", - Args: cobra.ExactArgs(1), - RunE: runMetadata, + Use: "imds [role_name]", + Aliases: []string{"metadata"}, + Short: "Run a local Instance Metadata Service (IMDS) endpoint that serves credentials", + Args: cobra.ExactArgs(1), + RunE: runMetadata, } func runMetadata(cmd *cobra.Command, args []string) error { - role = args[0] + role := args[0] client, err := creds.GetClient() if err != nil { return err diff --git a/cmd/root.go b/cmd/root.go index e5cc13a..69d938f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -49,6 +49,7 @@ func init() { cobra.OnInitialize(initConfig) cobra.OnInitialize(updateLoggingConfig) + rootCmd.PersistentFlags().BoolVarP(&noIpRestrict, "no-ip", "n", false, "remove IP restrictions") rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file (default is $HOME/.weep.yaml)") rootCmd.PersistentFlags().StringSliceVarP(&assumeRole, "assume-role", "A", make([]string, 0), "one or more roles to assume after retrieving credentials") rootCmd.PersistentFlags().StringVar(&logFormat, "log-format", "", "log format (json or tty)") diff --git a/cmd/serve.go b/cmd/serve.go new file mode 100644 index 0000000..610e9c6 --- /dev/null +++ b/cmd/serve.go @@ -0,0 +1,97 @@ +/* + * Copyright 2020 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cmd + +import ( + "fmt" + "net" + "net/http" + + "github.com/netflix/weep/cache" + "github.com/netflix/weep/creds" + + "github.com/spf13/viper" + + "github.com/gorilla/mux" + "github.com/netflix/weep/handlers" + "github.com/spf13/cobra" +) + +func init() { + serveCmd.PersistentFlags().StringVarP(&metadataRegion, "region", "r", "us-east-1", "region of metadata service") + serveCmd.PersistentFlags().StringVarP(&ecsProviderListenAddr, "listen-address", "a", "127.0.0.1", "IP address for the ECS credential provider to listen on") + serveCmd.PersistentFlags().IntVarP(&ecsProviderListenPort, "port", "p", viper.GetInt("server.ecs_credential_provider_port"), "port for the ECS credential provider service to listen on") + rootCmd.AddCommand(serveCmd) +} + +var serveCmd = &cobra.Command{ + Use: "serve [optional_role_name]", + Aliases: []string{"ecs_credential_provider", "metadata", "imds"}, + Short: serveShortHelp, + Long: serveLongHelp, + RunE: runWeepServer, +} + +func runWeepServer(cmd *cobra.Command, args []string) error { + ipaddress := net.ParseIP(ecsProviderListenAddr) + + if ipaddress == nil { + return fmt.Errorf("invalid IP: %s", ecsProviderListenAddr) + } + + listenAddr := fmt.Sprintf("%s:%d", ipaddress, ecsProviderListenPort) + + router := mux.NewRouter() + router.HandleFunc("/healthcheck", handlers.HealthcheckHandler) + + var role string + if len(args) > 0 { + role = args[0] + } + if role != "" { + log.Infof("Configuring weep IMDS service for role %s", role) + client, err := creds.GetClient() + if err != nil { + return err + } + err = cache.GlobalCache.SetDefault(client, role, metadataRegion, make([]string, 0)) + if err != nil { + return err + } + router.HandleFunc("/{version}/", handlers.CredentialServiceMiddleware(handlers.BaseVersionHandler)) + router.HandleFunc("/{version}/api/token", handlers.CredentialServiceMiddleware(handlers.TokenHandler)).Methods("PUT") + router.HandleFunc("/{version}/meta-data", handlers.CredentialServiceMiddleware(handlers.BaseHandler)) + router.HandleFunc("/{version}/meta-data/", handlers.CredentialServiceMiddleware(handlers.BaseHandler)) + router.HandleFunc("/{version}/meta-data/iam/info", handlers.CredentialServiceMiddleware(handlers.IamInfoHandler)) + router.HandleFunc("/{version}/meta-data/iam/security-credentials/", handlers.CredentialServiceMiddleware(handlers.RoleHandler)) + router.HandleFunc("/{version}/meta-data/iam/security-credentials/{role}", handlers.CredentialServiceMiddleware(handlers.CredentialsHandler)) + router.HandleFunc("/{version}/dynamic/instance-identity/document", handlers.CredentialServiceMiddleware(handlers.InstanceIdentityDocumentHandler)) + } + + router.HandleFunc("/ecs/{role:.*}", handlers.CredentialServiceMiddleware(handlers.ECSMetadataServiceCredentialsHandler)) + router.HandleFunc("/{path:.*}", handlers.CredentialServiceMiddleware(handlers.CustomHandler)) + + go func() { + log.Info("Starting weep on ", listenAddr) + log.Info(http.ListenAndServe(listenAddr, router)) + }() + + // Check for interrupt signal and exit cleanly + <-shutdown + log.Print("Shutdown signal received, stopping server...") + return nil +} diff --git a/cmd/service.go b/cmd/service.go index dbb5d6c..bf37e63 100644 --- a/cmd/service.go +++ b/cmd/service.go @@ -22,9 +22,11 @@ func init() { } var weepServiceControl = &cobra.Command{ - Use: "service [start|stop|restart|install|uninstall|run]", - Short: "Install or control weep as a system service", - RunE: runWeepServiceControl, + Use: "service [start|stop|restart|install|uninstall|run]", + Short: serviceShortHelp, + Long: serviceLongHelp, + RunE: runWeepServiceControl, + Hidden: true, } func runWeepServiceControl(cmd *cobra.Command, args []string) error { @@ -60,7 +62,7 @@ func (p *program) run() { args := viper.GetStringSlice("service.args") switch command := viper.GetString("service.command"); command { case "ecs_credential_provider": - err := runEcsMetadata(nil, args) + err := runWeepServer(nil, args) if err != nil { log.Error(err) exitCode = 1 diff --git a/cmd/setup.go b/cmd/setup.go index 0b69cf7..91ac8a6 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -26,11 +26,9 @@ func init() { var setupCmd = &cobra.Command{ Use: "setup", - Short: "Print setup information", + Short: setupShortHelp, + Long: setupLongHelp, Run: func(cmd *cobra.Command, args []string) { PrintSetup() }, } - -var fishSetup = `set -` diff --git a/cmd/vars.go b/cmd/vars.go index 63aa8d2..520f220 100644 --- a/cmd/vars.go +++ b/cmd/vars.go @@ -20,13 +20,14 @@ import "os" var ( assumeRole []string - role string profileName string destination string destinationConfig string force bool autoRefresh bool + generate bool noIpRestrict bool + showAll bool metadataRegion string metadataListenAddr string metadataListenPort int @@ -39,3 +40,72 @@ var ( shutdown chan os.Signal done chan int ) + +var completionShortHelp = "Generate completion script" +var completionLongHelp = `Generate shell completion script for Bash, Zsh, Fish, and Powershell. + +More information: https://hawkins.gitbook.io/consoleme/weep-cli/advanced-configuration/shell-completion +` +var credentialProcessShortHelp = "Retrieve credentials on the fly via the AWS SDK" +var credentialProcessLongHelp = `The credential_process command can be used by AWS SDKs to retrieve +credentials from Weep on the fly. The --generate flag lets you automatically +generate an AWS configuration with profiles for all of your available roles, or +you can manually update your configuration (see the link below to learn how). + +More information: https://hawkins.gitbook.io/consoleme/weep-cli/commands/credential-process +` + +var docsShortHelp = "Generate Markdown docs for CLI commands" +var docsLongHelp = `` + +var exportShortHelp = "Retrieve credentials to be exported as environment variables" +var exportLongHelp = `The export command retrieves credentials for a role and prints a shell command to export +the credentials to environment variables. + +More information: https://hawkins.gitbook.io/consoleme/weep-cli/commands/credential-export +` + +var fileShortHelp = "Retrieve credentials and save them to a credentials file" +var fileLongHelp = `The file command writes role credentials to the AWS credentials file, usually +~/.aws/credentials. Since these credentials are static, you’ll have to re-run the command +every hour to get new credentials. + +More information: https://hawkins.gitbook.io/consoleme/weep-cli/commands/credential-file +` + +var listShortHelp = "List available roles" +var listLongHelp = `The list command prints out all of the roles you have access to via ConsoleMe. By default, +this command will only show console roles. Use the --all flag to also include application +roles. + +More information: https://hawkins.gitbook.io/consoleme/weep-cli/commands/list-eligible-roles +` + +var serveShortHelp = "Run a local ECS Credential Provider endpoint that serves and caches credentials for roles on demand" +var serveLongHelp = `The serve command runs a local webserver that serves the /ecs/ path. When the +AWS_CONTAINER_CREDENTIALS_FULL_URI environment variable is set to a URL, the +AWS CLI and SDKs will use that URL to retrieve credentials. For example, if +you want to use credentials for a role called SuperCoolRole, you could do +something like this: + +AWS_CONTAINER_CREDENTIALS_FULL_URI=http://localhost:9091/ecs/SuperCoolRole \ + aws sts get-caller-identity + +If you just want to use a single role, use the --role argument to specify which one and it +will be served the same way credentials are served in an EC2 instance. There’s no need +to set an environment variable for this. + +More information: https://hawkins.gitbook.io/consoleme/weep-cli/commands/credential-provider +` + +var serviceShortHelp = "Install or control weep as a system service" +var serviceLongHelp = `EXPERIMENTAL FEATURE +The service command lets you install Weep as a service on a Linux, macOS, or Windows +system. +` + +var setupShortHelp = "Print setup information" +var setupLongHelp = `` + +var versionShortHelp = "Print version information" +var versionLongHelp = `` diff --git a/cmd/version.go b/cmd/version.go index 03f4671..c0f7053 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -30,7 +30,8 @@ func init() { var versionCmd = &cobra.Command{ Use: "version", - Short: "Print version information", + Short: versionShortHelp, + Long: versionLongHelp, Run: func(cmd *cobra.Command, args []string) { fmt.Println(metadata.GetVersion()) }, diff --git a/creds/consoleme.go b/creds/consoleme.go index e8840c1..332418e 100644 --- a/creds/consoleme.go +++ b/creds/consoleme.go @@ -132,7 +132,7 @@ func (c *Client) do(req *http.Request) (*http.Response, error) { // accounts returns all accounts, and allows you to filter the accounts by sub-resources // like: /accounts/service/support -func (c *Client) Roles() ([]string, error) { +func (c *Client) Roles(showAll bool) ([]string, error) { var cmCredentialErrorMessageType ConsolemeCredentialErrorMessageType req, err := c.buildRequest(http.MethodGet, "/get_roles?all=true", nil) if err != nil { @@ -141,7 +141,9 @@ func (c *Client) Roles() ([]string, error) { // Add URL Parameters q := url.Values{} - q.Add("all", "true") + if showAll { + q.Add("all", "true") + } req.URL.RawQuery = q.Encode() resp, err := c.do(req) diff --git a/handlers/credentialsHandler.go b/handlers/credentialsHandler.go index 1784c83..ced6d69 100644 --- a/handlers/credentialsHandler.go +++ b/handlers/credentialsHandler.go @@ -17,7 +17,6 @@ package handlers import ( - "bytes" "encoding/json" "fmt" "net/http" @@ -26,8 +25,12 @@ import ( ) func RoleHandler(w http.ResponseWriter, r *http.Request) { - // I think this works as long as there's a response? - fmt.Fprint(w, "hi") + defaultRole, err := cache.GlobalCache.GetDefault() + if err != nil { + fmt.Fprint(w, "error") + return + } + fmt.Fprint(w, defaultRole.Role) } func CredentialsHandler(w http.ResponseWriter, r *http.Request) { @@ -51,11 +54,8 @@ func CredentialsHandler(w http.ResponseWriter, r *http.Request) { Expiration: c.Expiration.UTC().Format("2006-01-02T15:04:05Z"), } - b, err := json.Marshal(credentialResponse) + err = json.NewEncoder(w).Encode(credentialResponse) if err != nil { - log.Error(err) + log.Errorf("failed to write response: %v", err) } - var out bytes.Buffer - json.Indent(&out, b, "", " ") - fmt.Fprintln(w, out.String()) } diff --git a/handlers/ecsCredentialsHandler.go b/handlers/ecsCredentialsHandler.go index d1caf03..2aa5e69 100644 --- a/handlers/ecsCredentialsHandler.go +++ b/handlers/ecsCredentialsHandler.go @@ -88,12 +88,8 @@ func ECSMetadataServiceCredentialsHandler(w http.ResponseWriter, r *http.Request Token: fmt.Sprintf("%s", cachedCredentials.SessionToken), } - b, err := json.Marshal(credentialResponse) + err = json.NewEncoder(w).Encode(credentialResponse) if err != nil { - log.Error(err) - } - _, err = w.Write(b) - if err != nil { - log.Errorf("failed to write HTTP response: %s", err) + log.Errorf("failed to write response: %v", err) } } diff --git a/handlers/iamInfoHandler.go b/handlers/iamInfoHandler.go index d895a5f..e419934 100644 --- a/handlers/iamInfoHandler.go +++ b/handlers/iamInfoHandler.go @@ -17,33 +17,29 @@ package handlers import ( - "bytes" "encoding/json" - "fmt" "net/http" + "github.com/netflix/weep/cache" + "github.com/netflix/weep/util" ) func IamInfoHandler(w http.ResponseWriter, r *http.Request) { - // TODO: this was crashing because of a nil pointer dereference. Fix it! - awsArn, _ := util.ArnParse("") + rawArn := cache.GlobalCache.DefaultArn() + awsArn, _ := util.ArnParse(rawArn) awsArn.ResourceType = "instance-profile" iamInfo := MetaDataIamInfoResponse{ - Code: "Success", - //LastUpdated: metadata.LastRenewal.UTC().Format("2006-01-02T15:04:05Z"), - LastUpdated: "", // TODO: fix this + Code: "Success", + LastUpdated: cache.GlobalCache.DefaultLastUpdated(), InstanceProfileARN: awsArn.ArnString(), InstanceProfileID: "AIPAI", } - b, err := json.Marshal(iamInfo) + err := json.NewEncoder(w).Encode(iamInfo) if err != nil { - log.Error(err) + log.Errorf("failed to write response: %v", err) } - var out bytes.Buffer - json.Indent(&out, b, "", " ") - fmt.Fprintln(w, out.String()) } diff --git a/handlers/instanceIdentityDocument.go b/handlers/instanceIdentityDocument.go index 41b4f62..02a8b6f 100644 --- a/handlers/instanceIdentityDocument.go +++ b/handlers/instanceIdentityDocument.go @@ -17,14 +17,12 @@ package handlers import ( - "bytes" "encoding/json" - "fmt" "net/http" - "time" - - "github.com/netflix/weep/creds" + "runtime" + "github.com/netflix/weep/cache" + "github.com/netflix/weep/metadata" "github.com/netflix/weep/util" ) @@ -33,8 +31,8 @@ var ( ) func InstanceIdentityDocumentHandler(w http.ResponseWriter, r *http.Request) { - // TODO: this was crashing because of a nil pointer dereference. Fix it! - awsArn, err := util.ArnParse("") + rawArn := cache.GlobalCache.DefaultArn() + awsArn, err := util.ArnParse(rawArn) if err != nil { accountID = "123456789012" @@ -54,19 +52,14 @@ func InstanceIdentityDocumentHandler(w http.ResponseWriter, r *http.Request) { KernelID: "aki-fc8f11cc", RamdiskID: "", AccountID: accountID, - Architecture: "x86_64", + Architecture: runtime.GOARCH, ImageID: "ami-12345", - //PendingTime: creds.Time(metadata.LastRenewal.UTC()), //.Format("2006-01-02T15:04:05Z"), - PendingTime: creds.Time(time.Now()), // TODO: fix this - Region: "", // TODO: set this based on config + PendingTime: metadata.StartupTime(), + Region: "", // TODO: set this based on config } - b, err := json.Marshal(identityDocument) + err = json.NewEncoder(w).Encode(identityDocument) if err != nil { - log.Error(err) + log.Errorf("failed to write response: %v", err) } - - var out bytes.Buffer - json.Indent(&out, b, "", " ") - fmt.Fprintln(w, out.String()) } diff --git a/handlers/types.go b/handlers/types.go index 2b83f43..e3503d0 100644 --- a/handlers/types.go +++ b/handlers/types.go @@ -1,7 +1,5 @@ package handlers -import "github.com/netflix/weep/creds" - type MetaDataCredentialResponse struct { Code string LastUpdated string @@ -28,19 +26,19 @@ type MetaDataIamInfoResponse struct { } type MetaDataInstanceIdentityDocumentResponse struct { - DevpayProductCodes []string `json:"devpayProductCodes"` - MarkerplaceProductCodes []string `json:"marketplaceProductCodes"` - PrivateIP string `json:"privateIp"` - Version string `json:"version"` - InstanceID string `json:"instanceId"` - BillingProductCodes []string `json:"billingProducts"` - InstanceType string `json:"instanceType"` - AvailabilityZone string `json:"availabilityZone"` - KernelID string `json:"kernelId"` - RamdiskID string `json:"ramdiskId"` - AccountID string `json:"accountId"` - Architecture string `json:"architecture"` - ImageID string `json:"imageId"` - PendingTime creds.Time `json:"pendingTime"` - Region string `json:"region"` + DevpayProductCodes []string `json:"devpayProductCodes"` + MarkerplaceProductCodes []string `json:"marketplaceProductCodes"` + PrivateIP string `json:"privateIp"` + Version string `json:"version"` + InstanceID string `json:"instanceId"` + BillingProductCodes []string `json:"billingProducts"` + InstanceType string `json:"instanceType"` + AvailabilityZone string `json:"availabilityZone"` + KernelID string `json:"kernelId"` + RamdiskID string `json:"ramdiskId"` + AccountID string `json:"accountId"` + Architecture string `json:"architecture"` + ImageID string `json:"imageId"` + PendingTime string `json:"pendingTime"` + Region string `json:"region"` } diff --git a/metadata/metadata.go b/metadata/metadata.go index 2d590b9..7e44288 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -28,9 +28,14 @@ var ( certCreationTime time.Time certFingerprint string weepMethod string + weepStartupTime time.Time log = logging.GetLogger() ) +func init() { + weepStartupTime = time.Now() +} + // GetInstanceInfo populates and returns an InstanceInfo, most likely to be used as // request metadata. func GetInstanceInfo() *InstanceInfo { @@ -46,6 +51,10 @@ func GetInstanceInfo() *InstanceInfo { return currentInstanceInfo } +func StartupTime() string { + return weepStartupTime.UTC().Format("2006-01-02T15:04:05Z") +} + func elapsedSeconds(startTime, endTime time.Time) int { if startTime.IsZero() || endTime.IsZero() { return 0 diff --git a/metadata/metadata_test.go b/metadata/metadata_test.go index 16cb6b5..66098e7 100644 --- a/metadata/metadata_test.go +++ b/metadata/metadata_test.go @@ -30,7 +30,9 @@ func TestGetInstanceInfo(t *testing.T) { if result.WeepVersion != Version { t.Errorf("weep version: expected %s, got %s", Version, result.WeepVersion) } - // TODO: check WeepMethod when that's implemented + if result.WeepMethod != weepMethod { + t.Errorf("weep method: expected %s, got %s", weepMethod, result.WeepMethod) + } } func TestElapsedSeconds(t *testing.T) { @@ -105,6 +107,23 @@ func TestUsername(t *testing.T) { expected := u.Username result := username() if result != expected { - t.Errorf("hostname failed: expected %s, got %s", expected, result) + t.Errorf("username failed: expected %s, got %s", expected, result) + } +} + +func TestSetWeepMethod(t *testing.T) { + expected := "cool-command" + SetWeepMethod(expected) + if weepMethod != expected { + t.Errorf("weep method failed: expected %s, got %s", expected, weepMethod) + } +} + +func TestStartupTime(t *testing.T) { + weepStartupTime = time.Date(2020, 1, 2, 3, 45, 67, 0, time.UTC) + expected := "2020-01-02T03:46:07Z" + result := StartupTime() + if result != expected { + t.Errorf("startup time failed: expected %s, got %s", expected, result) } }